Completed
Push — 8.x-2.x ( df78be...308518 )
by Frédéric G.
04:56 queued 04:51
created

Logger::requestEvents()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 18
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 31
rs 8.8571
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 Psr\Log\AbstractLogger;
14
use Psr\Log\LogLevel;
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
  protected $limit;
58
59
  /**
60
   * The message's placeholders parser.
61
   *
62
   * @var \Drupal\Core\Logger\LogMessageParserInterface
63
   */
64
  protected $parser;
65
66
  /**
67
   * An array of templates already used in this request.
68
   *
69
   * Used only with request tracking enabled.
70
   *
71
   * @var string[]
72
   */
73
  protected $templates = [];
74
75
  /**
76
   * A sequence number for log events during a request.
77
   *
78
   * @var int
79
   */
80
  protected $sequence = 0;
81
82
  /**
83
   * Logger constructor.
84
   *
85
   * @param \MongoDB\Database $database
86
   *   The database object.
87
   * @param \Drupal\Core\Logger\LogMessageParserInterface $parser
88
   *   The parser to use when extracting message variables.
89
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
90
   *   The core config_factory service.
91
   * @param \Symfony\Component\HttpFoundation\RequestStack $stack
92
   *   The core request_stack service.
93
   */
94
  public function __construct(Database $database, LogMessageParserInterface $parser, ConfigFactoryInterface $config_factory, RequestStack $stack) {
95
    $this->database = $database;
96
    $this->parser = $parser;
97
    $this->requestStack = $stack;
98
99
    $config = $config_factory->get(static::CONFIG_NAME);
100
    $this->limit = $config->get('limit');
101
    $this->items = $config->get('items');
102
    $this->requestTracking = $config->get('request_tracking');
103
  }
104
105
  /**
106
   * Fill in the log_entry function, file, and line.
107
   *
108
   * @param array $log_entry
109
   *   An event information to be logger.
110
   * @param array $backtrace
111
   *   A call stack.
112
   */
113
  protected function enhanceLogEntry(array &$log_entry, array $backtrace) {
114
    // Create list of functions to ignore in backtrace.
115
    static $ignored = array(
116
      'call_user_func_array' => 1,
117
      '_drupal_log_error' => 1,
118
      '_drupal_error_handler' => 1,
119
      '_drupal_error_handler_real' => 1,
120
      'Drupal\mongodb_watchdog\Logger::log' => 1,
121
      'Drupal\Core\Logger\LoggerChannel::log' => 1,
122
      'Drupal\Core\Logger\LoggerChannel::alert' => 1,
123
      'Drupal\Core\Logger\LoggerChannel::critical' => 1,
124
      'Drupal\Core\Logger\LoggerChannel::debug' => 1,
125
      'Drupal\Core\Logger\LoggerChannel::emergency' => 1,
126
      'Drupal\Core\Logger\LoggerChannel::error' => 1,
127
      'Drupal\Core\Logger\LoggerChannel::info' => 1,
128
      'Drupal\Core\Logger\LoggerChannel::notice' => 1,
129
      'Drupal\Core\Logger\LoggerChannel::warning' => 1,
130
    );
131
132
    foreach ($backtrace as $bt) {
133
      if (isset($bt['function'])) {
134
        $function = empty($bt['class']) ? $bt['function'] : $bt['class'] . '::' . $bt['function'];
135
        if (empty($ignored[$function])) {
136
          $log_entry['%function'] = $function;
137
          /* Some part of the stack, like the line or file info, may be missing.
138
           *
139
           * @see http://goo.gl/8s75df
140
           *
141
           * No need to fetch the line using reflection: it would be redundant
142
           * with the name of the function.
143
           */
144
          $log_entry['%line'] = isset($bt['line']) ? $bt['line'] : NULL;
145
          if (empty($bt['file'])) {
146
            $reflected_method = new \ReflectionMethod($function);
147
            $bt['file'] = $reflected_method->getFileName();
148
          }
149
150
          $log_entry['%file'] = $bt['file'];
151
          break;
152
        }
153
        elseif ($bt['function'] == '_drupal_exception_handler') {
154
          $e = $bt['args'][0];
155
          $this->enhanceLogEntry($log_entry, $e->getTrace());
156
        }
157
      }
158
    }
159
  }
160
161
  /**
162
   * {@inheritdoc}
163
   */
164
  public function log($level, $template, array $context = []) {
165
    if ($level > $this->limit) {
166
      return;
167
    }
168
169
    // Convert PSR3-style messages to SafeMarkup::format() style, so they can be
170
    // translated too in runtime.
171
    $message_placeholders = $this->parser->parseMessagePlaceholders($template, $context);
172
173
    // If code location information is all present, as for errors/exceptions,
174
    // then use it to build the message template id.
175
    $type = $context['channel'];
176
    $location_info = [
177
      '%type' => 1,
178
      '@message' => 1,
179
      '%function' => 1,
180
      '%file' => 1,
181
      '%line' => 1,
182
    ];
183
    if (!empty(array_diff_key($location_info, $message_placeholders))) {
184
      $this->enhanceLogEntry($message_placeholders, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10));
185
    }
186
    $file = $message_placeholders['%file'];
187
    $line = $message_placeholders['%line'];
188
    $function = $message_placeholders['%function'];
189
    $key = "${type}:${level}:${file}:${line}:${function}";
190
    $template_id = md5($key);
191
192
    $selector = ['_id' => $template_id];
193
    $update = [
194
      '$inc' => ['count' => 1],
195
      '$set' => [
196
        '_id' => $template_id,
197
        'message' => $template,
198
        'severity' => $level,
199
        'changed' => time(),
200
        'type' => Unicode::substr($context['channel'], 0, 64),
201
      ],
202
    ];
203
    $options = ['upsert' => TRUE];
204
    $template_result = $this->database
205
      ->selectCollection(static::TEMPLATE_COLLECTION)
206
      ->updateOne($selector, $update, $options);
207
    // Only add the template if if has not already been added.
208
    if ($this->requestTracking) {
209
      $request_id = $this->requestStack
210
        ->getCurrentRequest()
211
        ->server
212
        ->get('UNIQUE_ID');
213
214
      if (isset($this->templates[$template_id])) {
215
        $this->templates[$template_id]++;
216
      }
217
      else {
218
        $this->templates[$template_id] = 1;
219
        $selector = ['_id' => $request_id];
220
        $update = ['$addToSet' => ['templates' => $template_id]];
221
        $this->trackerCollection()->updateOne($selector, $update, $options);
222
      }
223
    }
224
225
    $event_collection = $this->eventCollection($template_id);
226
    if ($template_result->getUpsertedCount()) {
227
      // Capped collections are actually size-based, not count-based, so "items"
228
      // is only a maximum, assuming event documents weigh 1kB, but the actual
229
      // number of items stored may be lower if items are heavier.
230
      // We do not use 'autoindexid' for greater speed, because:
231
      // - it does not work on replica sets,
232
      // - it is deprecated in MongoDB 3.2 and going away in 3.4.
233
      $options = [
234
        'capped' => TRUE,
235
        'size' => $this->items * 1024,
236
        'max' => $this->items,
237
      ];
238
      $this->database->createCollection($event_collection->getCollectionName(), $options);
239
240
      // Do not create this index by default, as its cost is useless if request
241
      // tracking is not enabled.
242
      if ($this->requestTracking) {
243
        $key = ['requestTracking_id' => 1];
244
        $options = ['name' => 'admin-by-request'];
245
        $event_collection->createIndex($key, $options);
246
      }
247
    }
248
249
    foreach ($message_placeholders as &$placeholder) {
250
      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...
251
        $placeholder = Xss::filterAdmin($placeholder);
252
      }
253
    }
254
    $event = [
255
      'hostname' => Unicode::substr($context['ip'], 0, 128),
256
      'link' => $context['link'],
257
      'location' => $context['request_uri'],
258
      'referer' => $context['referer'],
259
      'timestamp' => $context['timestamp'],
260
      'user' => ['uid' => $context['uid']],
261
      'variables' => $message_placeholders,
262
    ];
263
    if ($this->requestTracking) {
264
      // Fetch the current request on each event to support subrequest nesting.
265
      $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...
266
      $event['requestTracking_sequence'] = $this->sequence;
267
      $this->sequence++;
268
    }
269
    $event_collection->insertOne($event);
270
  }
271
272
  /**
273
   * List the event collections.
274
   *
275
   * @return \MongoDB\Collection[]
276
   *   The collections with a name matching the event pattern.
277
   */
278
  public function eventCollections() {
279
    echo static::EVENT_COLLECTIONS_PATTERN;
280
    $options = [
281
      'filter' => [
282
        'name' => ['$regex' => static::EVENT_COLLECTIONS_PATTERN],
283
      ],
284
    ];
285
    $result = iterator_to_array($this->database->listCollections($options));
286
    return $result;
287
  }
288
289
  /**
290
   * Return a collection, given its template id.
291
   *
292
   * @param string $template_id
293
   *   The string representation of a template \MongoId.
294
   *
295
   * @return \MongoDB\Collection
296
   *   A collection object for the specified template id.
297
   */
298
  public function eventCollection($template_id) {
299
    $collection_name = static::EVENT_COLLECTION_PREFIX . $template_id;
300
    if (!preg_match('/' . static::EVENT_COLLECTIONS_PATTERN . '/', $collection_name)) {
301
      throw new InvalidArgumentException(t('Invalid watchdog template id `@id`.', [
302
        '@id' => $collection_name,
303
      ]));
304
    }
305
    $collection = $this->database->selectCollection($collection_name);
306
    return $collection;
307
  }
308
309
  /**
310
   * Ensure indexes are set on the collections.
311
   *
312
   * First index is on <line, timestamp> instead of <function, line, timestamp>,
313
   * because we write to this collection a lot, and the smaller index on two
314
   * numbers should be much faster to create than one with a string included.
315
   */
316
  public function ensureIndexes() {
317
    $indexes = [
318
      // Index for adding/updating increments.
319
      [
320
        'name' => 'for-increments',
321
        'key' => ['line' => 1, 'changed' => -1],
322
      ],
323
324
      // Index for overview page without filters.
325
      [
326
        'name' => 'overview-no-filters',
327
        'key' => ['changed' => -1],
328
      ],
329
330
      // Index for overview page filtering by type.
331
      [
332
        'name' => 'overview-by-type',
333
        'key' => ['type' => 1, 'changed' => -1],
334
      ],
335
336
      // Index for overview page filtering by severity.
337
      [
338
        'name' => 'overview-by-severity',
339
        'key' => ['severity' => 1, 'changed' => -1],
340
      ],
341
342
      // Index for overview page filtering by type and severity.
343
      [
344
        'name' => 'overview-by-both',
345
        'key' => ['type' => 1, 'severity' => 1, 'changed' => -1],
346
      ],
347
    ];
348
349
    $this->templateCollection()->createIndexes($indexes);
350
  }
351
352
  /**
353
   * Return the events having occurred during a given request.
354
   *
355
   * @param string $unsafe_request_id
356
   *   The raw request_id.
357
   * @return array<\Drupal\mongodb_watchdog\EventTemplate\Drupal\mongodb_watchdog\Event[]>
0 ignored issues
show
introduced by
Separate the @param and @return sections by a blank line.
Loading history...
358
   *   An array of [template, event] arrays, ordered by occurrence order.
359
   */
360
  public function requestEvents(string $unsafe_request_id) {
0 ignored issues
show
introduced by
Unknown type hint "string" found for $unsafe_request_id
Loading history...
361
    $templates = $this->requestTemplates($unsafe_request_id);
362
    $request_id = "$unsafe_request_id";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $unsafe_request_id 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...
363
    $selector = ['requestTracking_id' => $request_id];
364
    $events = [];
365
    $options = [
366
      'typeMap' => [
367
        'array' => 'array',
368
        'document' => 'array',
369
        'root' => '\Drupal\mongodb_watchdog\Event',
370
      ],
371
    ];
372
373
    /**
374
     * @var string $template_id
375
     * @var \Drupal\mongodb_watchdog\EventTemplate $template
376
     */
377
    foreach ($templates as $template_id => $template) {
378
      $event_collection = $this->eventCollection($template_id);
379
      $cursor = $event_collection->find($selector, $options);
380
      /** @var \Drupal\mongodb_watchdog\Event $event */
381
      foreach ($cursor as $event) {
382
        $events[$event->requestTracking_sequence] = [
383
          $template,
384
          $event,
385
        ];
386
      }
387
    }
388
389
    return $events;
390
  }
391
392
  public function requestTemplates(string $unsafe_request_id) {
0 ignored issues
show
introduced by
Missing function doc comment
Loading history...
393
    $request_id = "${unsafe_request_id}";
394
    $selector = ['_id' => $request_id];
395
396
    $doc = $this->trackerCollection()->findOne($selector, static::LEGACY_TYPE_MAP);
397
    if (empty($doc) || empty($doc['templates'])) {
398
      return [];
399
    }
400
401
    $selector = ['_id' => ['$in' => $doc['templates']]];
402
    $options = [
403
      'typeMap' => [
404
        'array' => 'array',
405
        'document' => 'array',
406
        'root' => '\Drupal\mongodb_watchdog\EventTemplate',
407
      ],
408
    ];
409
    $templates = [];
410
    $cursor = $this->templateCollection()->find($selector, $options);
411
    /** @var \Drupal\mongodb_watchdog\EventTemplate $template */
412
    foreach ($cursor as $template) {
413
      $templates[$template->_id] = $template;
414
    }
415
    return $templates;
416
  }
417
418
  /**
419
   * Return the request events tracker collection.
420
   *
421
   * @return \MongoDB\Collection
422
   *   The collection.
423
   */
424
  public function trackerCollection() {
425
    return $this->database->selectCollection(static::TRACKER_COLLECTION);
426
  }
427
428
  /**
429
   * Return the event templates collection.
430
   *
431
   * @return \MongoDB\Collection
432
   *   The collection.
433
   */
434
  public function templateCollection() {
435
    return $this->database->selectCollection(static::TEMPLATE_COLLECTION);
436
  }
437
438
  /**
0 ignored issues
show
introduced by
Missing short description in doc comment
Loading history...
439
   * @param string[] $types
0 ignored issues
show
introduced by
Missing parameter comment
Loading history...
440
   * @param string[]|int[] $levels
0 ignored issues
show
introduced by
Missing parameter comment
Loading history...
441
   *
442
   * @return \MongoDB\Driver\Cursor
0 ignored issues
show
introduced by
Return comment must be on the next line
Loading history...
443
   */
444
  public function templates(array $types = [], array $levels = []) {
445
    $selector = [];
446
    if (!empty($types)) {
447
      $selector['type'] = ['$in' => array_values($types)];
448
    }
449
    if (!empty($levels) && count($levels) !== count(RfcLogLevel::getLevels())) {
450
      // Severity levels come back from the session as strings, not integers.
451
      $selector['severity'] = ['$in' => array_values(array_map('intval', $levels))];
452
    }
453
    $options = [
454
      'sort' => [
455
        'count' => -1,
456
        'changed' => -1,
457
      ],
458
      'typeMap' => [
459
        'array' => 'array',
460
        'document' => 'array',
461
        'root' => '\Drupal\mongodb_watchdog\EventTemplate',
462
      ],
463
    ];
464
465
    $cursor = $this->templateCollection()->find($selector, $options);
466
    return $cursor;
467
  }
468
469
  /**
470
   * Return the template types actually present in storage.
471
   *
472
   * @return string[]
0 ignored issues
show
introduced by
Return comment must be on the next line
Loading history...
473
   */
474
  public function templateTypes() {
475
    $ret = $this->templateCollection()->distinct('type');
476
    return $ret;
477
  }
478
479
}
0 ignored issues
show
Coding Style introduced by
As per coding style, files should not end with a newline character.

This check marks files that end in a newline character, i.e. an empy line.

Loading history...
480