Completed
Push — 15-request_grouping ( b34bbe...76e174 )
by Frédéric G.
02:44
created

Logger   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 330
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 13
Bugs 0 Features 5
Metric Value
c 13
b 0
f 5
dl 0
loc 330
rs 10
wmc 24
lcom 1
cbo 3

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
C enhanceLogEntry() 0 47 8
C log() 0 93 9
A eventCollections() 0 10 1
A eventCollection() 0 10 2
B ensureIndexes() 0 35 1
A trackerCollection() 0 3 1
A templateCollection() 0 3 1
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 MongoDB\Database;
11
use MongoDB\Driver\Exception\InvalidArgumentException;
12
use Psr\Log\AbstractLogger;
13
use Symfony\Component\HttpFoundation\RequestStack;
14
15
/**
16
 * Class Logger is a PSR/3 Logger using a MongoDB data store.
17
 *
18
 * @package Drupal\mongodb_watchdog
19
 */
20
class Logger extends AbstractLogger {
21
  const CONFIG_NAME = 'mongodb_watchdog.settings';
22
23
  const TRACKER_COLLECTION = 'watchdog_tracker';
24
  const TEMPLATE_COLLECTION = 'watchdog';
25
  const EVENT_COLLECTION_PREFIX = 'watchdog_event_';
26
  const EVENT_COLLECTIONS_PATTERN = '^watchdog_event_[[:xdigit:]]{32}$';
27
28
  const LEGACY_TYPE_MAP = [
29
    'typeMap' => [
30
      'array' => 'array',
31
      'document' => 'array',
32
      'root' => 'array',
33
    ],
34
  ];
35
36
  /**
37
   * The logger storage.
38
   *
39
   * @var \MongoDB\Database
40
   */
41
  protected $database;
42
43
  /**
44
   * The limit for the capped event collections.
45
   *
46
   * @var int
47
   */
48
  protected $items;
49
50
  /**
51
   * The minimum logging level.
52
   *
53
   * @var int
54
   */
55
  protected $limit;
56
57
  /**
58
   * The message's placeholders parser.
59
   *
60
   * @var \Drupal\Core\Logger\LogMessageParserInterface
61
   */
62
  protected $parser;
63
64
  /**
65
   * An array of templates already used in this request.
66
   *
67
   * Used only with request tracking enabled.
68
   *
69
   * @var string[]
70
   */
71
  protected $templates = [];
72
73
  /**
74
   * Logger constructor.
75
   *
76
   * @param \MongoDB\Database $database
77
   *   The database object.
78
   * @param \Drupal\Core\Logger\LogMessageParserInterface $parser
79
   *   The parser to use when extracting message variables.
80
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
81
   *   The core config_factory service.
82
   * @param \Symfony\Component\HttpFoundation\RequestStack $stack
83
   *   The core request_stack service.
84
   */
85
  public function __construct(Database $database, LogMessageParserInterface $parser, ConfigFactoryInterface $config_factory, RequestStack $stack) {
86
    $this->database = $database;
87
    $this->parser = $parser;
88
    $this->requestStack = $stack;
89
90
    $config = $config_factory->get(static::CONFIG_NAME);
91
    $this->limit = $config->get('limit');
92
    $this->items = $config->get('items');
93
    $this->requestTracking = $config->get('request_tracking');
94
  }
95
96
  /**
97
   * Fill in the log_entry function, file, and line.
98
   *
99
   * @param array $log_entry
100
   *   An event information to be logger.
101
   * @param array $backtrace
102
   *   A call stack.
103
   */
104
  protected function enhanceLogEntry(array &$log_entry, array $backtrace) {
105
    // Create list of functions to ignore in backtrace.
106
    static $ignored = array(
107
      'call_user_func_array' => 1,
108
      '_drupal_log_error' => 1,
109
      '_drupal_error_handler' => 1,
110
      '_drupal_error_handler_real' => 1,
111
      'Drupal\mongodb_watchdog\Logger::log' => 1,
112
      'Drupal\Core\Logger\LoggerChannel::log' => 1,
113
      'Drupal\Core\Logger\LoggerChannel::alert' => 1,
114
      'Drupal\Core\Logger\LoggerChannel::critical' => 1,
115
      'Drupal\Core\Logger\LoggerChannel::debug' => 1,
116
      'Drupal\Core\Logger\LoggerChannel::emergency' => 1,
117
      'Drupal\Core\Logger\LoggerChannel::error' => 1,
118
      'Drupal\Core\Logger\LoggerChannel::info' => 1,
119
      'Drupal\Core\Logger\LoggerChannel::notice' => 1,
120
      'Drupal\Core\Logger\LoggerChannel::warning' => 1,
121
    );
122
123
    foreach ($backtrace as $bt) {
124
      if (isset($bt['function'])) {
125
        $function = empty($bt['class']) ? $bt['function'] : $bt['class'] . '::' . $bt['function'];
126
        if (empty($ignored[$function])) {
127
          $log_entry['%function'] = $function;
128
          /* Some part of the stack, like the line or file info, may be missing.
129
           *
130
           * @see http://goo.gl/8s75df
131
           *
132
           * No need to fetch the line using reflection: it would be redundant
133
           * with the name of the function.
134
           */
135
          $log_entry['%line'] = isset($bt['line']) ? $bt['line'] : NULL;
136
          if (empty($bt['file'])) {
137
            $reflected_method = new \ReflectionMethod($function);
138
            $bt['file'] = $reflected_method->getFileName();
139
          }
140
141
          $log_entry['%file'] = $bt['file'];
142
          break;
143
        }
144
        elseif ($bt['function'] == '_drupal_exception_handler') {
145
          $e = $bt['args'][0];
146
          $this->enhanceLogEntry($log_entry, $e->getTrace());
147
        }
148
      }
149
    }
150
  }
151
152
  /**
153
   * {@inheritdoc}
154
   */
155
  public function log($level, $template, array $context = []) {
156
    if ($level > $this->limit) {
157
      return;
158
    }
159
160
    // Convert PSR3-style messages to SafeMarkup::format() style, so they can be
161
    // translated too in runtime.
162
    $message_placeholders = $this->parser->parseMessagePlaceholders($template, $context);
163
164
    // If code location information is all present, as for errors/exceptions,
165
    // then use it to build the message template id.
166
    $type = $context['channel'];
167
    $location_info = [
168
      '%type' => 1,
169
      '@message' => 1,
170
      '%function' => 1,
171
      '%file' => 1,
172
      '%line' => 1,
173
    ];
174
    if (!empty(array_diff_key($location_info, $message_placeholders))) {
175
      $this->enhanceLogEntry($message_placeholders, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10));
176
    }
177
    $file = $message_placeholders['%file'];
178
    $line = $message_placeholders['%line'];
179
    $function = $message_placeholders['%function'];
180
    $key = "${type}:${level}:${file}:${line}:${function}";
181
    $template_id = md5($key);
182
183
    $selector = ['_id' => $template_id];
184
    $update = [
185
      '_id' => $template_id,
186
      'type' => Unicode::substr($context['channel'], 0, 64),
187
      'message' => $template,
188
      'severity' => $level,
189
    ];
190
    $options = ['upsert' => TRUE];
191
    $template_result = $this->database
192
      ->selectCollection(static::TEMPLATE_COLLECTION)
193
      ->replaceOne($selector, $update, $options);
194
    // Only add the template if if has not already been added.
195
    if ($this->requestTracking) {
196
      $request_id = $this->requestStack
197
        ->getCurrentRequest()
198
        ->server
199
        ->get('UNIQUE_ID');
200
201
      if (isset($this->templates[$template_id])) {
202
        $this->templates[$template_id]++;
203
      }
204
      else {
205
        $this->templates[$template_id] = 1;
206
        $selector = ['_id' => $request_id];
207
        $update = ['$addToSet' => ['templates' => $template_id]];
208
        $this->trackerCollection()->updateOne($selector, $update, $options);
209
      }
210
    }
211
212
    $event_collection = $this->eventCollection($template_id);
213
    if ($template_result->getUpsertedCount()) {
214
      // Capped collections are actually size-based, not count-based, so "items"
215
      // is only a maximum, assuming event documents weigh 1kB, but the actual
216
      // number of items stored may be lower if items are heavier.
217
      // We do not use 'autoindexid' for greater speed, because:
218
      // - it does not work on replica sets,
219
      // - it is deprecated in MongoDB 3.2 and going away in 3.4.
220
      $options = [
221
        'capped' => TRUE,
222
        'size' => $this->items * 1024,
223
        'max' => $this->items,
224
      ];
225
      $this->database->createCollection($event_collection->getCollectionName(), $options);
226
    }
227
228
    foreach ($message_placeholders as &$placeholder) {
229
      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...
230
        $placeholder = Xss::filterAdmin($placeholder);
231
      }
232
    }
233
    $event = [
234
      'hostname' => Unicode::substr($context['ip'], 0, 128),
235
      'link' => $context['link'],
236
      'location' => $context['request_uri'],
237
      'referer' => $context['referer'],
238
      'timestamp' => $context['timestamp'],
239
      'user' => ['uid' => $context['uid']],
240
      'variables' => $message_placeholders,
241
    ];
242
    if ($this->requestTracking) {
243
      // Fetch the current request on each event to support subrequest nesting.
244
      $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...
245
    }
246
    $event_collection->insertOne($event);
247
  }
248
249
  /**
250
   * List the event collections.
251
   *
252
   * @return \MongoDB\Collection[]
253
   *   The collections with a name matching the event pattern.
254
   */
255
  public function eventCollections() {
256
    echo static::EVENT_COLLECTIONS_PATTERN;
257
    $options = [
258
      'filter' => [
259
        'name' => ['$regex' => static::EVENT_COLLECTIONS_PATTERN],
260
      ],
261
    ];
262
    $result = iterator_to_array($this->database->listCollections($options));
263
    return $result;
264
  }
265
266
  /**
267
   * Return a collection, given its template id.
268
   *
269
   * @param string $template_id
270
   *   The string representation of a template \MongoId.
271
   *
272
   * @return \MongoDB\Collection
273
   *   A collection object for the specified template id.
274
   */
275
  public function eventCollection($template_id) {
276
    $collection_name = static::EVENT_COLLECTION_PREFIX . $template_id;
277
    if (!preg_match('/' . static::EVENT_COLLECTIONS_PATTERN . '/', $collection_name)) {
278
      throw new InvalidArgumentException(t('Invalid watchdog template id `@id`.', [
279
        '@id' => $collection_name,
280
      ]));
281
    }
282
    $collection = $this->database->selectCollection($collection_name);
283
    return $collection;
284
  }
285
286
  /**
287
   * Ensure indexes are set on the collections.
288
   *
289
   * First index is on <line, timestamp> instead of <function, line, timestamp>,
290
   * because we write to this collection a lot, and the smaller index on two
291
   * numbers should be much faster to create than one with a string included.
292
   */
293
  public function ensureIndexes() {
294
    $templates = $this->database->selectCollection(static::TEMPLATE_COLLECTION);
295
    $indexes = [
296
      // Index for adding/updating increments.
297
      [
298
        'name' => 'for-increments',
299
        'key' => ['line' => 1, 'timestamp' => -1],
300
      ],
301
302
      // Index for admin page without filters.
303
      [
304
        'name' => 'admin-no-filters',
305
        'key' => ['timestamp' => -1],
306
      ],
307
308
      // Index for admin page filtering by type.
309
      [
310
        'name' => 'admin-by-type',
311
        'key' => ['type' => 1, 'timestamp' => -1],
312
      ],
313
314
      // Index for admin page filtering by severity.
315
      [
316
        'name' => 'admin-by-severity',
317
        'key' => ['severity' => 1, 'timestamp' => -1],
318
      ],
319
320
      // Index for admin page filtering by type and severity.
321
      [
322
        'name' => 'admin-by-both',
323
        'key' => ['type' => 1, 'severity' => 1, 'timestamp' => -1],
324
      ],
325
    ];
326
    $templates->createIndexes($indexes);
327
  }
328
329
  /**
330
   * Return the request events tracker collection.
331
   *
332
   * @return \MongoDB\Collection
333
   *   The collection.
334
   */
335
  public function trackerCollection() {
336
    return $this->database->selectCollection(static::TRACKER_COLLECTION);
337
  }
338
339
  /**
340
   * Return the event templates collection.
341
   *
342
   * @return \MongoDB\Collection
343
   *   The collection.
344
   */
345
  public function templateCollection() {
346
    return $this->database->selectCollection(static::TEMPLATE_COLLECTION);
347
  }
348
349
}
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...
350