Passed
Pull Request — 8.x-2.x (#58)
by Frédéric G.
03:34
created

Logger::ensureCappedCollection()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 2
dl 0
loc 29
rs 9.456
c 0
b 0
f 0
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) {
0 ignored issues
show
Bug introduced by
The class Drupal\Component\Render\MarkupInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

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 the composer.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 or require-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 ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
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[]
0 ignored issues
show
Documentation introduced by
Should the return type not be array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
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