Completed
Pull Request — 8.x-2.x (#23)
by Frédéric G.
02:49
created

Logger::ensureSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 50
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 1
eloc 24
c 2
b 0
f 1
nc 1
nop 0
dl 0
loc 50
rs 9.3333
1
<?php
2
3
namespace Drupal\mongodb_watchdog;
4
5
use Drupal\Component\Render\MarkupInterface;
6
use Drupal\Component\Utility\Unicode;
7
use Drupal\Component\Utility\Xss;
8
use Drupal\Core\Config\ConfigFactoryInterface;
9
use Drupal\Core\Logger\LogMessageParserInterface;
10
use Drupal\Core\Logger\RfcLogLevel;
11
use MongoDB\Database;
12
use MongoDB\Driver\Exception\InvalidArgumentException;
13
use MongoDB\Driver\Exception\RuntimeException;
14
use Psr\Log\AbstractLogger;
15
use Symfony\Component\HttpFoundation\RequestStack;
16
17
/**
18
 * Class Logger is a PSR/3 Logger using a MongoDB data store.
19
 *
20
 * @package Drupal\mongodb_watchdog
21
 */
22
class Logger extends AbstractLogger {
23
  const CONFIG_NAME = 'mongodb_watchdog.settings';
24
25
  const TRACKER_COLLECTION = 'watchdog_tracker';
26
  const TEMPLATE_COLLECTION = 'watchdog';
27
  const EVENT_COLLECTION_PREFIX = 'watchdog_event_';
28
  const EVENT_COLLECTIONS_PATTERN = '^watchdog_event_[[:xdigit:]]{32}$';
29
30
  const LEGACY_TYPE_MAP = [
31
    'typeMap' => [
32
      'array' => 'array',
33
      'document' => 'array',
34
      'root' => 'array',
35
    ],
36
  ];
37
38
  /**
39
   * The logger storage.
40
   *
41
   * @var \MongoDB\Database
42
   */
43
  protected $database;
44
45
  /**
46
   * The limit for the capped event collections.
47
   *
48
   * @var int
49
   */
50
  protected $items;
51
52
  /**
53
   * The minimum logging level.
54
   *
55
   * @var int
56
   *
57
   * @see https://drupal.org/node/1355808
58
   */
59
  protected $limit = RfcLogLevel::DEBUG;
60
61
  /**
62
   * The message's placeholders parser.
63
   *
64
   * @var \Drupal\Core\Logger\LogMessageParserInterface
65
   */
66
  protected $parser;
67
68
  /**
69
   * The "requests" setting.
70
   *
71
   * @var int
72
   */
73
  protected $requests;
74
75
  /**
76
   * An array of templates already used in this request.
77
   *
78
   * Used only with request tracking enabled.
79
   *
80
   * @var string[]
81
   */
82
  protected $templates = [];
83
84
  /**
85
   * A sequence number for log events during a request.
86
   *
87
   * @var int
88
   */
89
  protected $sequence = 0;
90
91
  /**
92
   * Logger constructor.
93
   *
94
   * @param \MongoDB\Database $database
95
   *   The database object.
96
   * @param \Drupal\Core\Logger\LogMessageParserInterface $parser
97
   *   The parser to use when extracting message variables.
98
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
99
   *   The core config_factory service.
100
   * @param \Symfony\Component\HttpFoundation\RequestStack $stack
101
   *   The core request_stack service.
102
   */
103
  public function __construct(Database $database, LogMessageParserInterface $parser, ConfigFactoryInterface $config_factory, RequestStack $stack) {
104
    $this->database = $database;
105
    $this->parser = $parser;
106
    $this->requestStack = $stack;
0 ignored issues
show
Bug introduced by
The property requestStack does not seem to exist. Did you mean requests?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
107
108
    $config = $config_factory->get(static::CONFIG_NAME);
109
    $this->limit = $config->get('limit');
110
    $this->items = $config->get('items');
111
    $this->requests = $config->get('requests');
112
    $this->requestTracking = $config->get('request_tracking');
0 ignored issues
show
Bug introduced by
The property requestTracking does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
113
  }
114
115
  /**
116
   * Fill in the log_entry function, file, and line.
117
   *
118
   * @param array $log_entry
119
   *   An event information to be logger.
120
   * @param array $backtrace
121
   *   A call stack.
122
   */
123
  protected function enhanceLogEntry(array &$log_entry, array $backtrace) {
124
    // Create list of functions to ignore in backtrace.
125
    static $ignored = array(
126
      'call_user_func_array' => 1,
127
      '_drupal_log_error' => 1,
128
      '_drupal_error_handler' => 1,
129
      '_drupal_error_handler_real' => 1,
130
      'Drupal\mongodb_watchdog\Logger::log' => 1,
131
      'Drupal\Core\Logger\LoggerChannel::log' => 1,
132
      'Drupal\Core\Logger\LoggerChannel::alert' => 1,
133
      'Drupal\Core\Logger\LoggerChannel::critical' => 1,
134
      'Drupal\Core\Logger\LoggerChannel::debug' => 1,
135
      'Drupal\Core\Logger\LoggerChannel::emergency' => 1,
136
      'Drupal\Core\Logger\LoggerChannel::error' => 1,
137
      'Drupal\Core\Logger\LoggerChannel::info' => 1,
138
      'Drupal\Core\Logger\LoggerChannel::notice' => 1,
139
      'Drupal\Core\Logger\LoggerChannel::warning' => 1,
140
    );
141
142
    foreach ($backtrace as $bt) {
143
      if (isset($bt['function'])) {
144
        $function = empty($bt['class']) ? $bt['function'] : $bt['class'] . '::' . $bt['function'];
145
        if (empty($ignored[$function])) {
146
          $log_entry['%function'] = $function;
147
          /* Some part of the stack, like the line or file info, may be missing.
148
           *
149
           * @see http://goo.gl/8s75df
150
           *
151
           * No need to fetch the line using reflection: it would be redundant
152
           * with the name of the function.
153
           */
154
          $log_entry['%line'] = isset($bt['line']) ? $bt['line'] : NULL;
155
          if (empty($bt['file'])) {
156
            $reflected_method = new \ReflectionMethod($function);
157
            $bt['file'] = $reflected_method->getFileName();
158
          }
159
160
          $log_entry['%file'] = $bt['file'];
161
          break;
162
        }
163
        elseif ($bt['function'] == '_drupal_exception_handler') {
164
          $e = $bt['args'][0];
165
          $this->enhanceLogEntry($log_entry, $e->getTrace());
166
        }
167
      }
168
    }
169
  }
170
171
  /**
172
   * {@inheritdoc}
173
   *
174
   * @see https://drupal.org/node/1355808
175
   */
176
  public function log($level, $template, array $context = []) {
177
    if ($level > $this->limit) {
178
      return;
179
    }
180
181
    // Convert PSR3-style messages to SafeMarkup::format() style, so they can be
182
    // translated too in runtime.
183
    $message_placeholders = $this->parser->parseMessagePlaceholders($template, $context);
184
185
    // If code location information is all present, as for errors/exceptions,
186
    // then use it to build the message template id.
187
    $type = $context['channel'];
188
    $location_info = [
189
      '%type' => 1,
190
      '@message' => 1,
191
      '%function' => 1,
192
      '%file' => 1,
193
      '%line' => 1,
194
    ];
195
    if (!empty(array_diff_key($location_info, $message_placeholders))) {
196
      $this->enhanceLogEntry($message_placeholders, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10));
197
    }
198
    $file = $message_placeholders['%file'];
199
    $line = $message_placeholders['%line'];
200
    $function = $message_placeholders['%function'];
201
    $key = "${type}:${level}:${file}:${line}:${function}";
202
    $template_id = md5($key);
203
204
    $selector = ['_id' => $template_id];
205
    $update = [
206
      '$inc' => ['count' => 1],
207
      '$set' => [
208
        '_id' => $template_id,
209
        'message' => $template,
210
        'severity' => $level,
211
        'changed' => time(),
212
        'type' => Unicode::substr($context['channel'], 0, 64),
213
      ],
214
    ];
215
    $options = ['upsert' => TRUE];
216
    $template_result = $this->database
217
      ->selectCollection(static::TEMPLATE_COLLECTION)
218
      ->updateOne($selector, $update, $options);
219
220
    // Only insert each template once per request.
221
    if ($this->requestTracking && !isset($this->templates[$template_id])) {
222
      $request_id = $this->requestStack
0 ignored issues
show
Bug introduced by
The property requestStack does not seem to exist. Did you mean requests?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
223
        ->getCurrentRequest()
224
        ->server
225
        ->get('UNIQUE_ID');
226
227
      $this->templates[$template_id] = 1;
228
      $track = [
229
        'request_id' => $request_id,
230
        'template_id' => $template_id,
231
      ];
232
      $this->trackerCollection()->insertOne($track);
233
    }
234
235
    $event_collection = $this->eventCollection($template_id);
236
    if ($template_result->getUpsertedCount()) {
237
      // Capped collections are actually size-based, not count-based, so "items"
238
      // is only a maximum, assuming event documents weigh 1kB, but the actual
239
      // number of items stored may be lower if items are heavier.
240
      // We do not use 'autoindexid' for greater speed, because:
241
      // - it does not work on replica sets,
242
      // - it is deprecated in MongoDB 3.2 and going away in 3.4.
243
      $options = [
244
        'capped' => TRUE,
245
        'size' => $this->items * 1024,
246
        'max' => $this->items,
247
      ];
248
      $this->database->createCollection($event_collection->getCollectionName(), $options);
249
250
      // Do not create this index by default, as its cost is useless if request
251
      // tracking is not enabled.
252
      if ($this->requestTracking) {
253
        $key = ['requestTracking_id' => 1];
254
        $options = ['name' => 'admin-by-request'];
255
        $event_collection->createIndex($key, $options);
256
      }
257
    }
258
259
    foreach ($message_placeholders as &$placeholder) {
260
      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...
261
        $placeholder = Xss::filterAdmin($placeholder);
262
      }
263
    }
264
    $event = [
265
      'hostname' => Unicode::substr($context['ip'], 0, 128),
266
      'link' => $context['link'],
267
      'location' => $context['request_uri'],
268
      'referer' => $context['referer'],
269
      'timestamp' => $context['timestamp'],
270
      'user' => ['uid' => $context['uid']],
271
      'variables' => $message_placeholders,
272
    ];
273
    if ($this->requestTracking) {
274
      // Fetch the current request on each event to support subrequest nesting.
275
      $event['requestTracking_id'] = $request_id;
0 ignored issues
show
Bug introduced by
The variable $request_id does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
276
      $event['requestTracking_sequence'] = $this->sequence;
277
      $this->sequence++;
278
    }
279
    $event_collection->insertOne($event);
280
  }
281
282
  /**
283
   * Ensure a collection is capped with the proper size.
284
   *
285
   * @param string $name
286
   *   The collection name.
287
   * @param int $size
288
   *   The collection size cap.
289
   *
290
   * @return \MongoDB\Collection
291
   *   The collection, usable for additional commands like index creation.
292
   *
293
   * @TODO support sharded clusters: convertToCapped does not support them.
294
   *
295
   * @see https://docs.mongodb.com/manual/reference/command/convertToCapped
296
   *
297
   * Note that MongoDB 3.2 still misses a proper exists() command, which is the
298
   * reason for the weird try/catch logic.
299
   *
300
   * @see https://jira.mongodb.org/browse/SERVER-1938
301
   */
302
  public function ensureCappedCollection($name, $size) {
303
echo __LINE__ . "\n";
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 4 spaces, found 0
Loading history...
304
    if ($size == 0) {
305
      echo __LINE__ . " if size == 0\n";
306
      drupal_set_message('Abnormal size 0 ensuring capped collection, defaulting.', 'error');
0 ignored issues
show
introduced by
Messages are user facing text and must run through t() for translation
Loading history...
307
      $size = 100000;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $size. This often makes code more readable.
Loading history...
308
      echo __LINE__ . " size = 100k\n";
309
    }
310
    try {
311
      $command = [
312
        'collStats' => $name,
313
      ];
314
      echo __LINE__ . " try collstats\n";
315
      var_dump($command);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($command); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
316
      echo __LINE__ . "that was collstats command\n";
317
      $stats = $this->database->command($command, static::LEGACY_TYPE_MAP)->toArray()[0];
318
      echo __LINE__ . " after collstats command\n";
319
    }
320
    catch (RuntimeException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\RuntimeException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
321
      echo __LINE__ . " RT caught\n";
322
      // 59 is expected if the collection was not found. Other values are not.
323
      if ($e->getCode() !== 59) {
324
        echo __LINE__ . " exception not 59\n";
325
        throw $e;
326
        echo __LINE__ . " rethrown \n";
0 ignored issues
show
Unused Code introduced by
echo __LINE__ . ' rethrown '; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
327
      }
328
      echo __LINE__ . " exception 59, creating collection\n";
329
330
      $this->database->createCollection($name);
331
      echo __LINE__ . " collection $name created, trying collstats again\n";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $name instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
332
      $stats = $this->database->command([
333
        'collStats' => $name,
334
      ], static::LEGACY_TYPE_MAP)->toArray()[0];
335
      echo __LINE__ . " after stats command again\n";
336
    }
337
    echo __LINE__ . " after collstats try, selecting collection $name\n";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $name instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
338
339
    $collection = $this->database->selectCollection($name);
340
    echo __LINE__ . " collection $name selected\n";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $name instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
341
    if (!empty($stats['capped'])) {
342
      echo __LINE__ . " stats capped " . print_r($stats, true) . " returning collection\n";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal stats capped does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style introduced by
TRUE, FALSE and NULL should be uppercase as per the configured coding-style; instead of true please use TRUE.
Loading history...
343
      return $collection;
344
    }
345
346
    echo __LINE__ . " stats did not include capped\n";
347
    $command =  [
0 ignored issues
show
introduced by
Expected 1 space after "="; 2 found
Loading history...
348
      'convertToCapped' => $name,
349
      'size' => $size,
350
    ];
351
    var_dump($command);
352
    echo __LINE__ . " converttocapped command\n";
353
    $this->database->command($command);
354
    echo __LINE__ . " after capped command conversion\n";
355
    return $collection;
356
  }
357
358
  /**
359
   * Ensure indexes are set on the collections and tracker collection is capped.
360
   *
361
   * First index is on <line, timestamp> instead of <function, line, timestamp>,
362
   * because we write to this collection a lot, and the smaller index on two
363
   * numbers should be much faster to create than one with a string included.
364
   */
365
  public function ensureSchema() {
366
    echo "Ensuring trackerCollection" . static::TRACKER_COLLECTION . ", requests " . $this->requests * 1024 . "\n";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal Ensuring trackerCollection does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal , requests does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
367
    $trackerCollection = $this->ensureCappedCollection(static::TRACKER_COLLECTION, $this->requests * 1024);
368
    echo "After ensuring trackerCollection\n";
369
    $indexes = [
370
      [
371
        'name' => 'tracker-request',
372
        'key' => ['request_id' => 1],
373
      ],
374
    ];
375
    echo "Before createIndexes for tracker-request\n";
376
    $trackerCollection->createIndexes($indexes);
377
    echo "After createIndexes for tracker-request\n";
378
379
    $indexes = [
380
      // Index for adding/updating increments.
381
      [
382
        'name' => 'for-increments',
383
        'key' => ['line' => 1, 'changed' => -1],
384
      ],
385
386
      // Index for overview page without filters.
387
      [
388
        'name' => 'overview-no-filters',
389
        'key' => ['changed' => -1],
390
      ],
391
392
      // Index for overview page filtering by type.
393
      [
394
        'name' => 'overview-by-type',
395
        'key' => ['type' => 1, 'changed' => -1],
396
      ],
397
398
      // Index for overview page filtering by severity.
399
      [
400
        'name' => 'overview-by-severity',
401
        'key' => ['severity' => 1, 'changed' => -1],
402
      ],
403
404
      // Index for overview page filtering by type and severity.
405
      [
406
        'name' => 'overview-by-both',
407
        'key' => ['type' => 1, 'severity' => 1, 'changed' => -1],
408
      ],
409
    ];
410
411
    echo "Before createIndexes for templates collection\n";
412
    $this->templateCollection()->createIndexes($indexes);
413
    echo "After createIndexes for templates collection\n";
414
  }
415
416
  /**
417
   * Return a collection, given its template id.
418
   *
419
   * @param string $template_id
420
   *   The string representation of a template \MongoId.
421
   *
422
   * @return \MongoDB\Collection
423
   *   A collection object for the specified template id.
424
   */
425
  public function eventCollection($template_id) {
426
    $collection_name = static::EVENT_COLLECTION_PREFIX . $template_id;
427
    if (!preg_match('/' . static::EVENT_COLLECTIONS_PATTERN . '/', $collection_name)) {
428
      throw new InvalidArgumentException(t('Invalid watchdog template id `@id`.', [
429
        '@id' => $collection_name,
430
      ]));
431
    }
432
    $collection = $this->database->selectCollection($collection_name);
433
    return $collection;
434
  }
435
436
  /**
437
   * List the event collections.
438
   *
439
   * @return \MongoDB\Collection[]
440
   *   The collections with a name matching the event pattern.
441
   */
442
  public function eventCollections() {
443
    echo static::EVENT_COLLECTIONS_PATTERN;
444
    $options = [
445
      'filter' => [
446
        'name' => ['$regex' => static::EVENT_COLLECTIONS_PATTERN],
447
      ],
448
    ];
449
    $result = iterator_to_array($this->database->listCollections($options));
450
    return $result;
451
  }
452
453
  /**
454
   * Return the number of events for a template.
455
   *
456
   * @param \Drupal\mongodb_watchdog\EventTemplate $template
457
   *   A template for which to count events.
458
   *
459
   * @return int
460
   *   The number of matching events.
461
   */
462
  public function eventCount(EventTemplate $template) {
463
    return $this->eventCollection($template->_id)->count();
464
  }
465
466
  /**
467
   * Return the events having occurred during a given request.
468
   *
469
   * @param string $requestId
470
   *   The request unique_id.
471
   * @param int $skip
472
   *   The number of events to skip in the result.
473
   * @param int $limit
474
   *   The maximum number of events to return.
475
   *
476
   * @return array<\Drupal\mongodb_watchdog\EventTemplate\Drupal\mongodb_watchdog\Event[]>
477
   *   An array of [template, event] arrays, ordered by occurrence order.
478
   */
479
  public function requestEvents($requestId, $skip = 0, $limit = 0) {
480
    $templates = $this->requestTemplates($requestId);
481
    $selector = [
482
      'requestTracking_id' => $requestId,
483
      'requestTracking_sequence' => [
484
        '$gte' => $skip,
485
        '$lt' => $skip + $limit,
486
      ],
487
    ];
488
    $events = [];
489
    $options = [
490
      'typeMap' => [
491
        'array' => 'array',
492
        'document' => 'array',
493
        'root' => '\Drupal\mongodb_watchdog\Event',
494
      ],
495
    ];
496
497
    /**
498
     * @var string $template_id
499
     * @var \Drupal\mongodb_watchdog\EventTemplate $template
500
     */
501
    foreach ($templates as $template_id => $template) {
502
      $event_collection = $this->eventCollection($template_id);
503
      $cursor = $event_collection->find($selector, $options);
504
      /** @var \Drupal\mongodb_watchdog\Event $event */
505
      foreach ($cursor as $event) {
506
        $events[$event->requestTracking_sequence] = [
507
          $template,
508
          $event,
509
        ];
510
      }
511
    }
512
513
    ksort($events);
514
    return $events;
515
  }
516
517
  /**
518
   * Count events matching a request unique_id.
519
   *
520
   * XXX This implementation may be very inefficient in case of a request gone
521
   * bad generating non-templated varying messages: #requests is O(#templates).
522
   *
523
   * @param string $requestId
524
   *   The unique_id of the request.
525
   *
526
   * @return int
527
   *   The number of events matching the unique_id.
528
   */
529
  public function requestEventsCount($requestId) {
530
    if (empty($requestId)) {
531
      return 0;
532
    }
533
534
    $templates = $this->requestTemplates($requestId);
535
    $count = 0;
536
    foreach ($templates as $template) {
537
      $eventCollection = $this->eventCollection($template->_id);
538
      $selector = [
539
        'requestTracking_id' => $requestId,
540
      ];
541
      $count += $eventCollection->count($selector);
542
    }
543
544
    return $count;
545
  }
546
547
  /**
548
   * Return the number of event templates.
549
   */
550
  public function templatesCount() {
551
    return $this->templateCollection()->count([]);
552
  }
553
554
  /**
555
   * Return an array of templates uses during a given request.
556
   *
557
   * @param string $unsafe_request_id
558
   *   A request "unique_id".
559
   *
560
   * @return \Drupal\mongodb_watchdog\EventTemplate[]
561
   *   An array of EventTemplate instances.
562
   */
563
  public function requestTemplates($unsafe_request_id) {
564
    $request_id = "${unsafe_request_id}";
565
    $selector = [
566
      'request_id' => $request_id,
567
    ];
568
569
    $cursor = $this
570
      ->trackerCollection()
571
      ->find($selector, static::LEGACY_TYPE_MAP + [
572
        'projection' => [
573
          '_id' => 0,
574
          'template_id' => 1,
575
        ],
576
      ]);
577
    $template_ids = [];
578
    foreach ($cursor as $request) {
579
      $template_ids[] = $request['template_id'];
580
    }
581
    if (empty($template_ids)) {
582
      return [];
583
    }
584
585
    $selector = ['_id' => ['$in' => $template_ids]];
586
    $options = [
587
      'typeMap' => [
588
        'array' => 'array',
589
        'document' => 'array',
590
        'root' => '\Drupal\mongodb_watchdog\EventTemplate',
591
      ],
592
    ];
593
    $templates = [];
594
    $cursor = $this->templateCollection()->find($selector, $options);
595
    /** @var \Drupal\mongodb_watchdog\EventTemplate $template */
596
    foreach ($cursor as $template) {
597
      $templates[$template->_id] = $template;
598
    }
599
    return $templates;
600
  }
601
602
  /**
603
   * Return the request events tracker collection.
604
   *
605
   * @return \MongoDB\Collection
606
   *   The collection.
607
   */
608
  public function trackerCollection() {
609
    return $this->database->selectCollection(static::TRACKER_COLLECTION);
610
  }
611
612
  /**
613
   * Return the event templates collection.
614
   *
615
   * @return \MongoDB\Collection
616
   *   The collection.
617
   */
618
  public function templateCollection() {
619
    return $this->database->selectCollection(static::TEMPLATE_COLLECTION);
620
  }
621
622
  /**
623
   * Return templates matching type and level criteria.
624
   *
625
   * @param string[] $types
626
   *   An array of EventTemplate types. May be a hash.
627
   * @param string[]|int[] $levels
628
   *   An array of severity levels.
629
   * @param int $skip
630
   *   The number of templates to skip before the first one displayed.
631
   * @param int $limit
632
   *   The maximum number of templates to return.
633
   *
634
   * @return \MongoDB\Driver\Cursor
635
   *   A query result for the templates.
636
   */
637
  public function templates(array $types = [], array $levels = [], $skip = 0, $limit = 0) {
638
    $selector = [];
639
    if (!empty($types)) {
640
      $selector['type'] = ['$in' => array_values($types)];
641
    }
642
    if (!empty($levels) && count($levels) !== count(RfcLogLevel::getLevels())) {
643
      // Severity levels come back from the session as strings, not integers.
644
      $selector['severity'] = ['$in' => array_values(array_map('intval', $levels))];
645
    }
646
    $options = [
647
      'sort' => [
648
        'count' => -1,
649
        'changed' => -1,
650
      ],
651
      'typeMap' => [
652
        'array' => 'array',
653
        'document' => 'array',
654
        'root' => '\Drupal\mongodb_watchdog\EventTemplate',
655
      ],
656
    ];
657
    if ($skip) {
658
      $options['skip'] = $skip;
659
    }
660
    if ($limit) {
661
      $options['limit'] = $limit;
662
    }
663
664
    $cursor = $this->templateCollection()->find($selector, $options);
665
    return $cursor;
666
  }
667
668
  /**
669
   * Return the template types actually present in storage.
670
   *
671
   * @return string[]
672
   *   An array of distinct EventTemplate types.
673
   */
674
  public function templateTypes() {
675
    $ret = $this->templateCollection()->distinct('type');
676
    return $ret;
677
  }
678
679
}
680