1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types = 1); |
4
|
|
|
|
5
|
|
|
namespace Drupal\mongodb_watchdog; |
6
|
|
|
|
7
|
|
|
use Drupal\Component\Datetime\TimeInterface; |
8
|
|
|
use Drupal\Component\Render\MarkupInterface; |
9
|
|
|
use Drupal\Component\Utility\Xss; |
10
|
|
|
use Drupal\Core\Config\ConfigFactoryInterface; |
11
|
|
|
use Drupal\Core\Logger\LogMessageParserInterface; |
12
|
|
|
use Drupal\Core\Logger\RfcLogLevel; |
13
|
|
|
use Drupal\Core\Messenger\MessengerInterface; |
14
|
|
|
use Drupal\Core\StringTranslation\StringTranslationTrait; |
15
|
|
|
use Drupal\mongodb\MongoDb; |
16
|
|
|
use MongoDB\Collection; |
17
|
|
|
use MongoDB\Database; |
18
|
|
|
use MongoDB\Driver\Cursor; |
19
|
|
|
use MongoDB\Driver\Exception\InvalidArgumentException; |
20
|
|
|
use MongoDB\Driver\Exception\RuntimeException; |
21
|
|
|
use MongoDB\Driver\WriteConcern; |
22
|
|
|
use MongoDB\Model\CollectionInfoIterator; |
23
|
|
|
use Psr\Log\AbstractLogger; |
24
|
|
|
use Psr\Log\LogLevel; |
25
|
|
|
use Symfony\Component\HttpFoundation\RequestStack; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Class Logger is a PSR/3 Logger using a MongoDB data store. |
29
|
|
|
* |
30
|
|
|
* @package Drupal\mongodb_watchdog |
31
|
|
|
*/ |
32
|
|
|
class Logger extends AbstractLogger { |
33
|
|
|
use StringTranslationTrait; |
34
|
|
|
|
35
|
|
|
// Configuration-related constants. |
36
|
|
|
// The configuration item. |
37
|
|
|
const CONFIG_NAME = 'mongodb_watchdog.settings'; |
38
|
|
|
// The individual configuration keys. |
39
|
|
|
const CONFIG_ITEMS = 'items'; |
40
|
|
|
const CONFIG_REQUESTS = 'requests'; |
41
|
|
|
const CONFIG_LIMIT = 'limit'; |
42
|
|
|
const CONFIG_ITEMS_PER_PAGE = 'items_per_page'; |
43
|
|
|
const CONFIG_REQUEST_TRACKING = 'request_tracking'; |
44
|
|
|
|
45
|
|
|
// The logger database alias. |
46
|
|
|
const DB_LOGGER = 'logger'; |
47
|
|
|
|
48
|
|
|
// The default channel exposed when using the raw PSR-3 contract. |
49
|
|
|
const DEFAULT_CHANNEL = 'psr-3'; |
50
|
|
|
|
51
|
|
|
const MODULE = 'mongodb_watchdog'; |
52
|
|
|
|
53
|
|
|
// The service for the specific PSR-3 logger for MongoDB. |
54
|
|
|
const SERVICE_LOGGER = 'mongodb.logger'; |
55
|
|
|
// The service for the Drupal LoggerChannel for this module, logging to all |
56
|
|
|
// active loggers. |
57
|
|
|
const SERVICE_CHANNEL = 'logger.channel.mongodb_watchdog'; |
58
|
|
|
// The service for hook_requirements(). |
59
|
|
|
const SERVICE_REQUIREMENTS = 'mongodb.watchdog_requirements'; |
60
|
|
|
const SERVICE_SANITY_CHECK = 'mongodb.watchdog.sanity_check'; |
61
|
|
|
|
62
|
|
|
const TRACKER_COLLECTION = 'watchdog_tracker'; |
63
|
|
|
const TEMPLATE_COLLECTION = 'watchdog'; |
64
|
|
|
const EVENT_COLLECTION_PREFIX = 'watchdog_event_'; |
65
|
|
|
const EVENT_COLLECTIONS_PATTERN = '^watchdog_event_[[:xdigit:]]{32}$'; |
66
|
|
|
|
67
|
|
|
const LEGACY_TYPE_MAP = [ |
68
|
|
|
'typeMap' => [ |
69
|
|
|
'array' => 'array', |
70
|
|
|
'document' => 'array', |
71
|
|
|
'root' => 'array', |
72
|
|
|
], |
73
|
|
|
]; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Map of PSR3 log constants to RFC 5424 log constants. |
77
|
|
|
* |
78
|
|
|
* @var array |
79
|
|
|
* |
80
|
|
|
* @see \Drupal\Core\Logger\LoggerChannel |
81
|
|
|
* @see \Drupal\mongodb_watchdog\Logger::log() |
82
|
|
|
*/ |
83
|
|
|
protected $rfc5424levels = [ |
84
|
|
|
LogLevel::EMERGENCY => RfcLogLevel::EMERGENCY, |
85
|
|
|
LogLevel::ALERT => RfcLogLevel::ALERT, |
86
|
|
|
LogLevel::CRITICAL => RfcLogLevel::CRITICAL, |
87
|
|
|
LogLevel::ERROR => RfcLogLevel::ERROR, |
88
|
|
|
LogLevel::WARNING => RfcLogLevel::WARNING, |
89
|
|
|
LogLevel::NOTICE => RfcLogLevel::NOTICE, |
90
|
|
|
LogLevel::INFO => RfcLogLevel::INFO, |
91
|
|
|
LogLevel::DEBUG => RfcLogLevel::DEBUG, |
92
|
|
|
]; |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* The logger storage. |
96
|
|
|
* |
97
|
|
|
* @var \MongoDB\Database |
98
|
|
|
*/ |
99
|
|
|
protected $database; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* The limit for the capped event collections. |
103
|
|
|
* |
104
|
|
|
* @var int |
105
|
|
|
*/ |
106
|
|
|
protected $items; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* The minimum logging level. |
110
|
|
|
* |
111
|
|
|
* @var int |
112
|
|
|
* |
113
|
|
|
* @see https://drupal.org/node/1355808 |
114
|
|
|
*/ |
115
|
|
|
protected $limit = RfcLogLevel::DEBUG; |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* The messenger service. |
119
|
|
|
* |
120
|
|
|
* @var \Drupal\Core\Messenger\MessengerInterface |
121
|
|
|
*/ |
122
|
|
|
protected $messenger; |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* The message's placeholders parser. |
126
|
|
|
* |
127
|
|
|
* @var \Drupal\Core\Logger\LogMessageParserInterface |
128
|
|
|
*/ |
129
|
|
|
protected $parser; |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* The "requests" setting. |
133
|
|
|
* |
134
|
|
|
* @var int |
135
|
|
|
*/ |
136
|
|
|
protected $requests; |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* The request_stack service. |
140
|
|
|
* |
141
|
|
|
* @var \Symfony\Component\HttpFoundation\RequestStack |
142
|
|
|
*/ |
143
|
|
|
protected $requestStack; |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* A sequence number for log events during a request. |
147
|
|
|
* |
148
|
|
|
* @var int |
149
|
|
|
*/ |
150
|
|
|
protected $sequence = 0; |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* An array of templates already used in this request. |
154
|
|
|
* |
155
|
|
|
* Used only with request tracking enabled. |
156
|
|
|
* |
157
|
|
|
* @var string[] |
158
|
|
|
*/ |
159
|
|
|
protected $templates = []; |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* The datetime.time service. |
163
|
|
|
* |
164
|
|
|
* @var \Drupal\Component\Datetime\TimeInterface |
165
|
|
|
*/ |
166
|
|
|
protected $time; |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Logger constructor. |
170
|
|
|
* |
171
|
|
|
* @param \MongoDB\Database $database |
172
|
|
|
* The database object. |
173
|
|
|
* @param \Drupal\Core\Logger\LogMessageParserInterface $parser |
174
|
|
|
* The parser to use when extracting message variables. |
175
|
|
|
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory |
176
|
|
|
* The core config_factory service. |
177
|
|
|
* @param \Symfony\Component\HttpFoundation\RequestStack $stack |
178
|
|
|
* The core request_stack service. |
179
|
|
|
* @param \Drupal\Core\Messenger\MessengerInterface $messenger |
180
|
|
|
* The messenger service. |
181
|
|
|
* @param \Drupal\Component\Datetime\TimeInterface $time |
182
|
|
|
* The datetime.time service. |
183
|
|
|
*/ |
184
|
|
|
public function __construct( |
185
|
|
|
Database $database, |
186
|
|
|
LogMessageParserInterface $parser, |
187
|
|
|
ConfigFactoryInterface $configFactory, |
188
|
|
|
RequestStack $stack, |
189
|
|
|
MessengerInterface $messenger, |
190
|
|
|
TimeInterface $time |
191
|
|
|
) { |
192
|
|
|
$this->database = $database; |
193
|
|
|
$this->messenger = $messenger; |
194
|
|
|
$this->parser = $parser; |
195
|
|
|
$this->requestStack = $stack; |
196
|
|
|
$this->time = $time; |
197
|
|
|
|
198
|
|
|
$config = $configFactory->get(static::CONFIG_NAME); |
199
|
|
|
// During install, a logger will be invoked 3 times, the first 2 without any |
200
|
|
|
// configuration information, so hard-coded defaults are needed on all |
201
|
|
|
// config keys. |
202
|
|
|
$this->setLimit($config->get(static::CONFIG_LIMIT) ?? RfcLogLevel::DEBUG); |
203
|
|
|
// Do NOT use 1E4 / 1E5: these are doubles, but config is typed to integers. |
204
|
|
|
$this->items = $config->get(static::CONFIG_ITEMS) ?? 10000; |
205
|
|
|
$this->requests = $config->get(static::CONFIG_REQUESTS) ?? 100000; |
206
|
|
|
$this->requestTracking = $config->get(static::CONFIG_REQUEST_TRACKING) ?? FALSE; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Fill in the log_entry function, file, and line. |
211
|
|
|
* |
212
|
|
|
* @param array $entry |
213
|
|
|
* An event information to be logger. |
214
|
|
|
* @param array $backtrace |
215
|
|
|
* A call stack. |
216
|
|
|
* |
217
|
|
|
* @throws \ReflectionException |
218
|
|
|
*/ |
219
|
|
|
protected function enhanceLogEntry(array &$entry, array $backtrace): void { |
220
|
|
|
// Create list of functions to ignore in backtrace. |
221
|
|
|
static $ignored = [ |
222
|
|
|
'call_user_func_array' => 1, |
223
|
|
|
'_drupal_log_error' => 1, |
224
|
|
|
'_drupal_error_handler' => 1, |
225
|
|
|
'_drupal_error_handler_real' => 1, |
226
|
|
|
'Drupal\mongodb_watchdog\Logger::log' => 1, |
227
|
|
|
'Drupal\Core\Logger\LoggerChannel::log' => 1, |
228
|
|
|
'Drupal\Core\Logger\LoggerChannel::alert' => 1, |
229
|
|
|
'Drupal\Core\Logger\LoggerChannel::critical' => 1, |
230
|
|
|
'Drupal\Core\Logger\LoggerChannel::debug' => 1, |
231
|
|
|
'Drupal\Core\Logger\LoggerChannel::emergency' => 1, |
232
|
|
|
'Drupal\Core\Logger\LoggerChannel::error' => 1, |
233
|
|
|
'Drupal\Core\Logger\LoggerChannel::info' => 1, |
234
|
|
|
'Drupal\Core\Logger\LoggerChannel::notice' => 1, |
235
|
|
|
'Drupal\Core\Logger\LoggerChannel::warning' => 1, |
236
|
|
|
]; |
237
|
|
|
|
238
|
|
|
foreach ($backtrace as $bt) { |
239
|
|
|
if (isset($bt['function'])) { |
240
|
|
|
$function = empty($bt['class']) ? $bt['function'] : $bt['class'] . '::' . $bt['function']; |
241
|
|
|
if (empty($ignored[$function])) { |
242
|
|
|
$entry['%function'] = $function; |
243
|
|
|
/* Some part of the stack, like the line or file info, may be missing. |
244
|
|
|
* |
245
|
|
|
* @see http://goo.gl/8s75df |
246
|
|
|
* |
247
|
|
|
* No need to fetch the line using reflection: it would be redundant |
248
|
|
|
* with the name of the function. |
249
|
|
|
*/ |
250
|
|
|
$entry['%line'] = isset($bt['line']) ? $bt['line'] : NULL; |
251
|
|
|
if (empty($bt['file'])) { |
252
|
|
|
$method = new \ReflectionMethod($function); |
253
|
|
|
$bt['file'] = $method->getFileName(); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
$entry['%file'] = $bt['file']; |
257
|
|
|
break; |
258
|
|
|
} |
259
|
|
|
elseif ($bt['function'] == '_drupal_exception_handler') { |
260
|
|
|
$e = $bt['args'][0]; |
261
|
|
|
$this->enhanceLogEntry($entry, $e->getTrace()); |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* {@inheritdoc} |
269
|
|
|
* |
270
|
|
|
* @see https://httpd.apache.org/docs/2.4/en/mod/mod_unique_id.html |
271
|
|
|
*/ |
272
|
|
|
public function log($level, $template, array $context = []): void { |
273
|
|
|
// PSR-3 LoggerInterface documents level as "mixed", while the RFC itself |
274
|
|
|
// in §1.1 implies implementations may know about non-standard levels. In |
275
|
|
|
// the case of Drupal implementations, this includes the 8 RFC5424 levels. |
276
|
|
|
if (is_string($level)) { |
277
|
|
|
$level = $this->rfc5424levels[$level]; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
if ($level > $this->limit) { |
281
|
|
|
return; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
// Convert PSR3-style messages to SafeMarkup::format() style, so they can be |
285
|
|
|
// translated too in runtime. |
286
|
|
|
$placeholders = $this->parser->parseMessagePlaceholders($template, $context); |
287
|
|
|
|
288
|
|
|
// If code location information is all present, as for errors/exceptions, |
289
|
|
|
// then use it to build the message template id. |
290
|
|
|
$type = $context['channel'] ?? static::DEFAULT_CHANNEL; |
291
|
|
|
$location = [ |
292
|
|
|
'%type' => 1, |
293
|
|
|
'@message' => 1, |
294
|
|
|
'%function' => 1, |
295
|
|
|
'%file' => 1, |
296
|
|
|
'%line' => 1, |
297
|
|
|
]; |
298
|
|
|
if (!empty(array_diff_key($location, $placeholders))) { |
299
|
|
|
$this->enhanceLogEntry($placeholders, |
300
|
|
|
debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10)); |
301
|
|
|
} |
302
|
|
|
$file = $placeholders['%file']; |
303
|
|
|
$line = $placeholders['%line']; |
304
|
|
|
$function = $placeholders['%function']; |
305
|
|
|
$key = implode(":", [$type, $level, $file, $line, $function]); |
306
|
|
|
$templateId = md5($key); |
307
|
|
|
|
308
|
|
|
$selector = ['_id' => $templateId]; |
309
|
|
|
$update = [ |
310
|
|
|
'$inc' => ['count' => 1], |
311
|
|
|
'$set' => [ |
312
|
|
|
'_id' => $templateId, |
313
|
|
|
'message' => $template, |
314
|
|
|
'severity' => $level, |
315
|
|
|
'changed' => $this->time->getCurrentTime(), |
316
|
|
|
'type' => mb_substr($type, 0, 64), |
317
|
|
|
], |
318
|
|
|
]; |
319
|
|
|
$options = ['upsert' => TRUE]; |
320
|
|
|
$templateResult = $this->database |
321
|
|
|
->selectCollection(static::TEMPLATE_COLLECTION) |
322
|
|
|
->updateOne($selector, $update, $options); |
323
|
|
|
|
324
|
|
|
// Only insert each template once per request. |
325
|
|
|
if ($this->requestTracking && !isset($this->templates[$templateId])) { |
326
|
|
|
$requestId = $this->requestStack |
327
|
|
|
->getCurrentRequest() |
328
|
|
|
->server |
329
|
|
|
->get('UNIQUE_ID'); |
330
|
|
|
|
331
|
|
|
$this->templates[$templateId] = 1; |
332
|
|
|
$track = [ |
333
|
|
|
'requestId' => $requestId, |
334
|
|
|
'templateId' => $templateId, |
335
|
|
|
]; |
336
|
|
|
$this->trackerCollection()->insertOne($track); |
337
|
|
|
} |
338
|
|
|
else { |
339
|
|
|
// 24-byte format like mod_unique_id values. |
340
|
|
|
$requestId = '@@Not-a-valid-request@@'; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
$eventCollection = $this->eventCollection($templateId); |
344
|
|
|
if ($templateResult->getUpsertedCount()) { |
345
|
|
|
// Capped collections are actually size-based, not count-based, so "items" |
346
|
|
|
// is only a maximum, assuming event documents weigh 1kB, but the actual |
347
|
|
|
// number of items stored may be lower if items are heavier. |
348
|
|
|
// We do not use 'autoindexid' for greater speed, because: |
349
|
|
|
// - it does not work on replica sets, |
350
|
|
|
// - it is deprecated in MongoDB 3.2 and going away in 3.4. |
351
|
|
|
$options = [ |
352
|
|
|
'capped' => TRUE, |
353
|
|
|
'size' => $this->items * 1024, |
354
|
|
|
'max' => $this->items, |
355
|
|
|
]; |
356
|
|
|
$this->database->createCollection($eventCollection->getCollectionName(), $options); |
357
|
|
|
|
358
|
|
|
// Do not create this index by default, as its cost is useless if request |
359
|
|
|
// tracking is not enabled. |
360
|
|
|
if ($this->requestTracking) { |
361
|
|
|
$key = ['requestTracking_id' => 1]; |
362
|
|
|
$options = ['name' => 'admin-by-request']; |
363
|
|
|
$eventCollection->createIndex($key, $options); |
364
|
|
|
} |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
foreach ($placeholders as &$placeholder) { |
368
|
|
|
if ($placeholder instanceof MarkupInterface) { |
|
|
|
|
369
|
|
|
$placeholder = Xss::filterAdmin($placeholder); |
370
|
|
|
} |
371
|
|
|
} |
372
|
|
|
$event = [ |
373
|
|
|
'hostname' => mb_substr($context['ip'] ?? '', 0, 128), |
374
|
|
|
'link' => $context['link'] ?? NULL, |
375
|
|
|
'location' => $context['request_uri'] ?? NULL, |
376
|
|
|
'referer' => $context['referer'] ?? NULL, |
377
|
|
|
'timestamp' => $context['timestamp'] ?? $this->time->getCurrentTime(), |
378
|
|
|
'user' => ['uid' => $context['uid'] ?? 0], |
379
|
|
|
'variables' => $placeholders, |
380
|
|
|
]; |
381
|
|
|
if ($this->requestTracking) { |
382
|
|
|
// Fetch the current request on each event to support subrequest nesting. |
383
|
|
|
$event['requestTracking_id'] = $requestId; |
384
|
|
|
$event['requestTracking_sequence'] = $this->sequence; |
385
|
|
|
$this->sequence++; |
386
|
|
|
} |
387
|
|
|
$eventCollection->insertOne($event); |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* Ensure a collection is capped with the proper size. |
392
|
|
|
* |
393
|
|
|
* @param string $name |
394
|
|
|
* The collection name. |
395
|
|
|
* @param int $size |
396
|
|
|
* The collection size cap. |
397
|
|
|
* |
398
|
|
|
* @return \MongoDB\Collection |
399
|
|
|
* The collection, usable for additional commands like index creation. |
400
|
|
|
* |
401
|
|
|
* @TODO support sharded clusters: convertToCapped does not support them. |
402
|
|
|
* |
403
|
|
|
* @throws \MongoDB\Exception\InvalidArgumentException |
404
|
|
|
* @throws \MongoDB\Exception\UnsupportedException |
405
|
|
|
* @throws \MongoDB\Exception\UnexpectedValueException |
406
|
|
|
* @throws \MongoDB\Driver\Exception\RuntimeException |
407
|
|
|
* |
408
|
|
|
* @see https://docs.mongodb.com/manual/reference/command/convertToCapped |
409
|
|
|
* |
410
|
|
|
* Note that MongoDB 4.2 still misses a proper exists() command, which is the |
411
|
|
|
* reason for the weird try/catch logic. |
412
|
|
|
* |
413
|
|
|
* @see https://jira.mongodb.org/browse/SERVER-1938 |
414
|
|
|
*/ |
415
|
|
|
public function ensureCappedCollection(string $name, int $size): Collection { |
416
|
|
|
if ($size === 0) { |
417
|
|
|
$this->messenger->addError($this->t('Abnormal size 0 ensuring capped collection, defaulting.')); |
418
|
|
|
$size = 100000; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
$collection = $this->ensureCollection($name); |
422
|
|
|
|
423
|
|
|
$command = [ |
424
|
|
|
'collStats' => $name, |
425
|
|
|
]; |
426
|
|
|
$stats = $this->database |
427
|
|
|
->command($command, static::LEGACY_TYPE_MAP) |
428
|
|
|
->toArray()[0]; |
429
|
|
|
if (!empty($stats['capped'])) { |
430
|
|
|
return $collection; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
$command = [ |
434
|
|
|
'convertToCapped' => $name, |
435
|
|
|
'size' => $size, |
436
|
|
|
]; |
437
|
|
|
$this->database->command($command); |
438
|
|
|
$this->messenger->addStatus($this->t('@name converted to capped collection size @size.', [ |
439
|
|
|
'@name' => $name, |
440
|
|
|
'@size' => $size, |
441
|
|
|
])); |
442
|
|
|
return $collection; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Ensure a collection exists in the logger database. |
447
|
|
|
* |
448
|
|
|
* - If it already existed, it will not lose any data. |
449
|
|
|
* - If it gets created, it will be empty. |
450
|
|
|
* |
451
|
|
|
* @param string $name |
452
|
|
|
* The name of the collection. |
453
|
|
|
* |
454
|
|
|
* @return \MongoDB\Collection |
455
|
|
|
* The chosen collection, guaranteed to exist. |
456
|
|
|
* |
457
|
|
|
* @throws \MongoDB\Exception\InvalidArgumentException |
458
|
|
|
* @throws \MongoDB\Exception\UnsupportedException |
459
|
|
|
* @throws \MongoDB\Exception\UnexpectedValueException |
460
|
|
|
* @throws \MongoDB\Driver\Exception\RuntimeException |
461
|
|
|
*/ |
462
|
|
|
public function ensureCollection(string $name): Collection { |
463
|
|
|
$collection = $this->database |
464
|
|
|
->selectCollection($name); |
465
|
|
|
$count = $collection->countDocuments(); |
466
|
|
|
// Nothing to do if it already contains documents. |
467
|
|
|
if ($count > 0) { |
468
|
|
|
return $collection; |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
// Since the MongoDB API has no way to check whether a collection exists |
472
|
|
|
// without listing the database, and no longer exposes an API to |
473
|
|
|
// differentiate between a nonexistent collection and an empty one, we |
474
|
|
|
// insert dummy data to force creation of the collection, and possibly even |
475
|
|
|
// the database, as in some versions (noticed on 4.2 WT engine) the database |
476
|
|
|
// is dropped when its last collection is. |
477
|
|
|
$res = $collection->insertOne([ |
478
|
|
|
'_id' => 'dummy', |
479
|
|
|
[ |
480
|
|
|
// Ensure later operations are actually run after the server writes. |
481
|
|
|
// See https://docs.mongodb.com/manual/reference/write-concern/#acknowledgment-behavior |
482
|
|
|
'writeConcern' => [ |
483
|
|
|
'w' => WriteConcern::MAJORITY, |
484
|
|
|
'j' => TRUE, |
485
|
|
|
], |
486
|
|
|
], |
487
|
|
|
]); |
488
|
|
|
// With these options, all writes should be acknowledged. |
489
|
|
|
if (!$res->isAcknowledged()) { |
490
|
|
|
throw new RuntimeException("Failed inserting document during ensureCollection"); |
491
|
|
|
} |
492
|
|
|
// That document should not persist. |
493
|
|
|
$collection->deleteMany([]); |
494
|
|
|
return $collection; |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
/** |
498
|
|
|
* Ensure indexes are set on the collections and tracker collection is capped. |
499
|
|
|
* |
500
|
|
|
* First index is on <line, timestamp> instead of <function, line, timestamp>, |
501
|
|
|
* because we write to this collection a lot, and the smaller index on two |
502
|
|
|
* numbers should be much faster to create than one with a string included. |
503
|
|
|
*/ |
504
|
|
|
public function ensureSchema(): void { |
505
|
|
|
$trackerCollection = $this->ensureCappedCollection(static::TRACKER_COLLECTION, $this->requests * 1024); |
506
|
|
|
$indexes = [ |
507
|
|
|
[ |
508
|
|
|
'name' => 'tracker-request', |
509
|
|
|
'key' => ['request_id' => 1], |
510
|
|
|
], |
511
|
|
|
]; |
512
|
|
|
$trackerCollection->createIndexes($indexes); |
513
|
|
|
|
514
|
|
|
$indexes = [ |
515
|
|
|
// Index for adding/updating increments. |
516
|
|
|
[ |
517
|
|
|
'name' => 'for-increments', |
518
|
|
|
'key' => ['line' => 1, 'changed' => -1], |
519
|
|
|
], |
520
|
|
|
|
521
|
|
|
// Index for overview page without filters. |
522
|
|
|
[ |
523
|
|
|
'name' => 'overview-no-filters', |
524
|
|
|
'key' => ['changed' => -1], |
525
|
|
|
], |
526
|
|
|
|
527
|
|
|
// Index for overview page filtering by type. |
528
|
|
|
[ |
529
|
|
|
'name' => 'overview-by-type', |
530
|
|
|
'key' => ['type' => 1, 'changed' => -1], |
531
|
|
|
], |
532
|
|
|
|
533
|
|
|
// Index for overview page filtering by severity. |
534
|
|
|
[ |
535
|
|
|
'name' => 'overview-by-severity', |
536
|
|
|
'key' => ['severity' => 1, 'changed' => -1], |
537
|
|
|
], |
538
|
|
|
|
539
|
|
|
// Index for overview page filtering by type and severity. |
540
|
|
|
[ |
541
|
|
|
'name' => 'overview-by-both', |
542
|
|
|
'key' => ['type' => 1, 'severity' => 1, 'changed' => -1], |
543
|
|
|
], |
544
|
|
|
]; |
545
|
|
|
|
546
|
|
|
$this->templateCollection()->createIndexes($indexes); |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
/** |
550
|
|
|
* Return a collection, given its template id. |
551
|
|
|
* |
552
|
|
|
* @param string $templateId |
553
|
|
|
* The string representation of a template \MongoId. |
554
|
|
|
* |
555
|
|
|
* @return \MongoDB\Collection |
556
|
|
|
* A collection object for the specified template id. |
557
|
|
|
*/ |
558
|
|
|
public function eventCollection($templateId): Collection { |
559
|
|
|
$name = static::EVENT_COLLECTION_PREFIX . $templateId; |
560
|
|
|
if (!preg_match('/' . static::EVENT_COLLECTIONS_PATTERN . '/', $name)) { |
561
|
|
|
throw new InvalidArgumentException($this->t('Invalid watchdog template id `@id`.', [ |
562
|
|
|
'@id' => $name, |
563
|
|
|
])); |
564
|
|
|
} |
565
|
|
|
$collection = $this->database->selectCollection($name); |
566
|
|
|
return $collection; |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
/** |
570
|
|
|
* List the event collections. |
571
|
|
|
* |
572
|
|
|
* @return \MongoDB\Model\CollectionInfoIterator |
573
|
|
|
* The collections with a name matching the event pattern. |
574
|
|
|
*/ |
575
|
|
|
public function eventCollections() : CollectionInfoIterator { |
576
|
|
|
$options = [ |
577
|
|
|
'filter' => [ |
578
|
|
|
'name' => ['$regex' => static::EVENT_COLLECTIONS_PATTERN], |
579
|
|
|
], |
580
|
|
|
]; |
581
|
|
|
$result = $this->database->listCollections($options); |
582
|
|
|
return $result; |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
/** |
586
|
|
|
* Return the number of events for a template. |
587
|
|
|
* |
588
|
|
|
* @param \Drupal\mongodb_watchdog\EventTemplate $template |
589
|
|
|
* A template for which to count events. |
590
|
|
|
* |
591
|
|
|
* @return int |
592
|
|
|
* The number of matching events. |
593
|
|
|
*/ |
594
|
|
|
public function eventCount(EventTemplate $template) : int { |
595
|
|
|
return MongoDb::countCollection($this->eventCollection($template->_id)); |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* Return the events having occurred during a given request. |
600
|
|
|
* |
601
|
|
|
* @param string $requestId |
602
|
|
|
* The request unique_id. |
603
|
|
|
* @param int $skip |
604
|
|
|
* The number of events to skip in the result. |
605
|
|
|
* @param int $limit |
606
|
|
|
* The maximum number of events to return. |
607
|
|
|
* |
608
|
|
|
* @return \Drupal\mongodb_watchdog\EventTemplate|\Drupal\mongodb_watchdog\Event[] |
|
|
|
|
609
|
|
|
* An array of [template, event] arrays, ordered by occurrence order. |
610
|
|
|
*/ |
611
|
|
|
public function requestEvents($requestId, $skip = 0, $limit = 0): array { |
612
|
|
|
$templates = $this->requestTemplates($requestId); |
613
|
|
|
$selector = [ |
614
|
|
|
'requestTracking_id' => $requestId, |
615
|
|
|
'requestTracking_sequence' => [ |
616
|
|
|
'$gte' => $skip, |
617
|
|
|
'$lt' => $skip + $limit, |
618
|
|
|
], |
619
|
|
|
]; |
620
|
|
|
$events = []; |
621
|
|
|
$options = [ |
622
|
|
|
'typeMap' => [ |
623
|
|
|
'array' => 'array', |
624
|
|
|
'document' => 'array', |
625
|
|
|
'root' => '\Drupal\mongodb_watchdog\Event', |
626
|
|
|
], |
627
|
|
|
]; |
628
|
|
|
|
629
|
|
|
// @var string $templateId |
630
|
|
|
// @var \Drupal\mongodb_watchdog\EventTemplate $template |
631
|
|
|
foreach ($templates as $templateId => $template) { |
632
|
|
|
$eventCollection = $this->eventCollection($templateId); |
633
|
|
|
$cursor = $eventCollection->find($selector, $options); |
634
|
|
|
/** @var \Drupal\mongodb_watchdog\Event $event */ |
635
|
|
|
foreach ($cursor as $event) { |
636
|
|
|
$events[$event->requestTracking_sequence] = [ |
637
|
|
|
$template, |
638
|
|
|
$event, |
639
|
|
|
]; |
640
|
|
|
} |
641
|
|
|
} |
642
|
|
|
|
643
|
|
|
ksort($events); |
644
|
|
|
return $events; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
/** |
648
|
|
|
* Count events matching a request unique_id. |
649
|
|
|
* |
650
|
|
|
* XXX This implementation may be very inefficient in case of a request gone |
651
|
|
|
* bad generating non-templated varying messages: #requests is O(#templates). |
652
|
|
|
* |
653
|
|
|
* @param string $requestId |
654
|
|
|
* The unique_id of the request. |
655
|
|
|
* |
656
|
|
|
* @return int |
657
|
|
|
* The number of events matching the unique_id. |
658
|
|
|
*/ |
659
|
|
|
public function requestEventsCount($requestId): int { |
660
|
|
|
if (empty($requestId)) { |
661
|
|
|
return 0; |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
$templates = $this->requestTemplates($requestId); |
665
|
|
|
$count = 0; |
666
|
|
|
foreach ($templates as $template) { |
667
|
|
|
$eventCollection = $this->eventCollection($template->_id); |
668
|
|
|
$selector = [ |
669
|
|
|
'requestTracking_id' => $requestId, |
670
|
|
|
]; |
671
|
|
|
$count += MongoDb::countCollection($eventCollection, $selector); |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
return $count; |
675
|
|
|
} |
676
|
|
|
|
677
|
|
|
/** |
678
|
|
|
* Setter for limit. |
679
|
|
|
* |
680
|
|
|
* @param int $limit |
681
|
|
|
* The limit value. |
682
|
|
|
*/ |
683
|
|
|
public function setLimit(int $limit): void { |
684
|
|
|
$this->limit = $limit; |
685
|
|
|
} |
686
|
|
|
|
687
|
|
|
/** |
688
|
|
|
* Return the number of event templates. |
689
|
|
|
* |
690
|
|
|
* @throws \ReflectionException |
691
|
|
|
*/ |
692
|
|
|
public function templatesCount(): int { |
693
|
|
|
return MongoDb::countCollection($this->templateCollection()); |
694
|
|
|
} |
695
|
|
|
|
696
|
|
|
/** |
697
|
|
|
* Return an array of templates uses during a given request. |
698
|
|
|
* |
699
|
|
|
* @param string $unsafeRequestId |
700
|
|
|
* A request "unique_id". |
701
|
|
|
* |
702
|
|
|
* @return \Drupal\mongodb_watchdog\EventTemplate[] |
703
|
|
|
* An array of EventTemplate instances. |
704
|
|
|
* |
705
|
|
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter) |
706
|
|
|
* @see https://github.com/phpmd/phpmd/issues/561 |
707
|
|
|
*/ |
708
|
|
|
public function requestTemplates($unsafeRequestId): array { |
709
|
|
|
$selector = [ |
710
|
|
|
// Variable quoted to avoid passing an object and risk a NoSQL injection. |
711
|
|
|
'requestId' => "${unsafeRequestId}", |
712
|
|
|
]; |
713
|
|
|
|
714
|
|
|
$cursor = $this |
715
|
|
|
->trackerCollection() |
716
|
|
|
->find($selector, static::LEGACY_TYPE_MAP + [ |
717
|
|
|
'projection' => [ |
718
|
|
|
'_id' => 0, |
719
|
|
|
'template_id' => 1, |
720
|
|
|
], |
721
|
|
|
]); |
722
|
|
|
$templateIds = []; |
723
|
|
|
foreach ($cursor as $request) { |
724
|
|
|
$templateIds[] = $request['template_id']; |
725
|
|
|
} |
726
|
|
|
if (empty($templateIds)) { |
727
|
|
|
return []; |
728
|
|
|
} |
729
|
|
|
|
730
|
|
|
$selector = ['_id' => ['$in' => $templateIds]]; |
731
|
|
|
$options = [ |
732
|
|
|
'typeMap' => [ |
733
|
|
|
'array' => 'array', |
734
|
|
|
'document' => 'array', |
735
|
|
|
'root' => '\Drupal\mongodb_watchdog\EventTemplate', |
736
|
|
|
], |
737
|
|
|
]; |
738
|
|
|
$templates = []; |
739
|
|
|
$cursor = $this->templateCollection()->find($selector, $options); |
740
|
|
|
/** @var \Drupal\mongodb_watchdog\EventTemplate $template */ |
741
|
|
|
foreach ($cursor as $template) { |
742
|
|
|
$templates[$template->_id] = $template; |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
return $templates; |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
/** |
749
|
|
|
* Return the request events tracker collection. |
750
|
|
|
* |
751
|
|
|
* @return \MongoDB\Collection |
752
|
|
|
* The collection. |
753
|
|
|
*/ |
754
|
|
|
public function trackerCollection(): Collection { |
755
|
|
|
return $this->database->selectCollection(static::TRACKER_COLLECTION); |
756
|
|
|
} |
757
|
|
|
|
758
|
|
|
/** |
759
|
|
|
* Return the event templates collection. |
760
|
|
|
* |
761
|
|
|
* @return \MongoDB\Collection |
762
|
|
|
* The collection. |
763
|
|
|
*/ |
764
|
|
|
public function templateCollection(): Collection { |
765
|
|
|
return $this->database->selectCollection(static::TEMPLATE_COLLECTION); |
766
|
|
|
} |
767
|
|
|
|
768
|
|
|
/** |
769
|
|
|
* Return templates matching type and level criteria. |
770
|
|
|
* |
771
|
|
|
* @param string[] $types |
772
|
|
|
* An array of EventTemplate types. May be a hash. |
773
|
|
|
* @param string[]|int[] $levels |
774
|
|
|
* An array of severity levels. |
775
|
|
|
* @param int $skip |
776
|
|
|
* The number of templates to skip before the first one displayed. |
777
|
|
|
* @param int $limit |
778
|
|
|
* The maximum number of templates to return. |
779
|
|
|
* |
780
|
|
|
* @return \MongoDB\Driver\Cursor |
781
|
|
|
* A query result for the templates. |
782
|
|
|
*/ |
783
|
|
|
public function templates(array $types = [], array $levels = [], $skip = 0, $limit = 0): Cursor { |
784
|
|
|
$selector = []; |
785
|
|
|
if (!empty($types)) { |
786
|
|
|
$selector['type'] = ['$in' => array_values($types)]; |
787
|
|
|
} |
788
|
|
|
if (!empty($levels) && count($levels) !== count(RfcLogLevel::getLevels())) { |
789
|
|
|
// Severity levels come back from the session as strings, not integers. |
790
|
|
|
$selector['severity'] = ['$in' => array_values(array_map('intval', $levels))]; |
791
|
|
|
} |
792
|
|
|
$options = [ |
793
|
|
|
'sort' => [ |
794
|
|
|
'count' => -1, |
795
|
|
|
'changed' => -1, |
796
|
|
|
], |
797
|
|
|
'typeMap' => [ |
798
|
|
|
'array' => 'array', |
799
|
|
|
'document' => 'array', |
800
|
|
|
'root' => '\Drupal\mongodb_watchdog\EventTemplate', |
801
|
|
|
], |
802
|
|
|
]; |
803
|
|
|
if ($skip) { |
804
|
|
|
$options['skip'] = $skip; |
805
|
|
|
} |
806
|
|
|
if ($limit) { |
807
|
|
|
$options['limit'] = $limit; |
808
|
|
|
} |
809
|
|
|
|
810
|
|
|
$cursor = $this->templateCollection()->find($selector, $options); |
811
|
|
|
return $cursor; |
812
|
|
|
} |
813
|
|
|
|
814
|
|
|
/** |
815
|
|
|
* Return the template types actually present in storage. |
816
|
|
|
* |
817
|
|
|
* @return string[] |
818
|
|
|
* An array of distinct EventTemplate types. |
819
|
|
|
*/ |
820
|
|
|
public function templateTypes(): array { |
821
|
|
|
$ret = $this->templateCollection()->distinct('type'); |
822
|
|
|
return $ret; |
823
|
|
|
} |
824
|
|
|
|
825
|
|
|
} |
826
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.json
file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.json
to be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
require
orrequire-dev
section?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceof
checks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.