Completed
Push — watchdog-config_level ( 2edb6e...17c4dd )
by Frédéric G.
08:32 queued 05:27
created

Logger::enhanceLogEntry()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 47
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 8
eloc 30
c 2
b 0
f 1
nc 14
nop 2
dl 0
loc 47
rs 5.7377
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
14
/**
15
 * Class Logger is a PSR/3 Logger using a MongoDB data store.
16
 *
17
 * @package Drupal\mongodb_watchdog
18
 */
19
class Logger extends AbstractLogger {
20
  const CONFIG_NAME = 'mongodb_watchdog.settings';
21
22
  const TEMPLATE_COLLECTION = 'watchdog';
23
  const EVENT_COLLECTION_PREFIX = 'watchdog_event_';
24
  const EVENT_COLLECTIONS_PATTERN = '^watchdog_event_[[:xdigit:]]{32}$';
25
26
  const LEGACY_TYPE_MAP = [
27
    'typeMap' => [
28
      'array' => 'array',
29
      'document' => 'array',
30
      'root' => 'array',
31
    ],
32
  ];
33
34
  /**
35
   * The logger storage.
36
   *
37
   * @var \MongoDB\Database
38
   */
39
  protected $database;
40
41
  /**
42
   * The minimum logging level.
43
   *
44
   * @var int
45
   */
46
  protected $limit;
47
48
  /**
49
   * The message's placeholders parser.
50
   *
51
   * @var \Drupal\Core\Logger\LogMessageParserInterface
52
   */
53
  protected $parser;
54
55
  /**
56
   * Constructs a Logger object.
57
   *
58
   * @param \MongoDB\Database $database
59
   *   The database object.
60
   * @param \Drupal\Core\Logger\LogMessageParserInterface $parser
61
   *   The parser to use when extracting message variables.
62
   */
63
  public function __construct(Database $database, LogMessageParserInterface $parser, ConfigFactoryInterface $config) {
64
    $this->database = $database;
65
    $this->parser = $parser;
66
    $this->limit = $config->get(static::CONFIG_NAME)->get('limit');
67
  }
68
69
  /**
70
   * Fill in the log_entry function, file, and line.
71
   *
72
   * @param array $log_entry
73
   *   An event information to be logger.
74
   * @param array $backtrace
75
   *   A call stack.
76
   */
77
  protected function enhanceLogEntry(array &$log_entry, array $backtrace) {
78
    // Create list of functions to ignore in backtrace.
79
    static $ignored = array(
80
      'call_user_func_array' => 1,
81
      '_drupal_log_error' => 1,
82
      '_drupal_error_handler' => 1,
83
      '_drupal_error_handler_real' => 1,
84
      'Drupal\mongodb_watchdog\Logger::log' => 1,
85
      'Drupal\Core\Logger\LoggerChannel::log' => 1,
86
      'Drupal\Core\Logger\LoggerChannel::alert' => 1,
87
      'Drupal\Core\Logger\LoggerChannel::critical' => 1,
88
      'Drupal\Core\Logger\LoggerChannel::debug' => 1,
89
      'Drupal\Core\Logger\LoggerChannel::emergency' => 1,
90
      'Drupal\Core\Logger\LoggerChannel::error' => 1,
91
      'Drupal\Core\Logger\LoggerChannel::info' => 1,
92
      'Drupal\Core\Logger\LoggerChannel::notice' => 1,
93
      'Drupal\Core\Logger\LoggerChannel::warning' => 1,
94
    );
95
96
    foreach ($backtrace as $bt) {
97
      if (isset($bt['function'])) {
98
        $function = empty($bt['class']) ? $bt['function'] : $bt['class'] . '::' . $bt['function'];
99
        if (empty($ignored[$function])) {
100
          $log_entry['%function'] = $function;
101
          /* Some part of the stack, like the line or file info, may be missing.
102
           *
103
           * @see http://goo.gl/8s75df
104
           *
105
           * No need to fetch the line using reflection: it would be redundant
106
           * with the name of the function.
107
           */
108
          $log_entry['%line'] = isset($bt['line']) ? $bt['line'] : NULL;
109
          if (empty($bt['file'])) {
110
            $reflected_method = new \ReflectionMethod($function);
111
            $bt['file'] = $reflected_method->getFileName();
112
          }
113
114
          $log_entry['%file'] = $bt['file'];
115
          break;
116
        }
117
        elseif ($bt['function'] == '_drupal_exception_handler') {
118
          $e = $bt['args'][0];
119
          $this->enhanceLogEntry($log_entry, $e->getTrace());
120
        }
121
      }
122
    }
123
  }
124
125
  /**
126
   * {@inheritdoc}
127
   */
128
  public function log($level, $template, array $context = []) {
129
    if ($level > $this->limit) {
130
      return;
131
    }
132
    
133
    // Convert PSR3-style messages to SafeMarkup::format() style, so they can be
134
    // translated too in runtime.
135
    $message_placeholders = $this->parser->parseMessagePlaceholders($template, $context);
136
137
    // If code location information is all present, as for errors/exceptions,
138
    // then use it to build the message template id.
139
    $type = $context['channel'];
140
    $location_info = [
141
      '%type' => 1,
142
      '@message' => 1,
143
      '%function' => 1,
144
      '%file' => 1,
145
      '%line' => 1,
146
    ];
147
    if (!empty(array_diff_key($location_info, $message_placeholders))) {
148
      $this->enhanceLogEntry($message_placeholders, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10));
149
    }
150
    $file = $message_placeholders['%file'];
151
    $line = $message_placeholders['%line'];
152
    $function = $message_placeholders['%function'];
153
    $key = "${type}:${level}:${file}:${line}:${function}";
154
    $template_id = md5($key);
155
156
    $selector = ['_id' => $template_id];
157
    $update = [
158
      '_id' => $template_id,
159
      'type' => Unicode::substr($context['channel'], 0, 64),
160
      'message' => $template,
161
      'severity' => $level,
162
    ];
163
    $options = ['upsert' => TRUE];
164
    $template_result = $this->database
165
      ->selectCollection(static::TEMPLATE_COLLECTION)
166
      ->replaceOne($selector, $update, $options);
167
    $template_result->getUpsertedId();
168
169
    $event_collection = $this->eventCollection($template_id);
170
    foreach ($message_placeholders as &$placeholder) {
171
      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...
172
        $placeholder = Xss::filterAdmin($placeholder);
173
      }
174
    }
175
    $event = [
176
      'hostname' => Unicode::substr($context['ip'], 0, 128),
177
      'link' => $context['link'],
178
      'location' => $context['request_uri'],
179
      'referer' => $context['referer'],
180
      'timestamp' => $context['timestamp'],
181
      'user' => ['uid' => $context['uid']],
182
      'variables' => $message_placeholders,
183
    ];
184
    $event_collection->insertOne($event);
185
  }
186
187
  /**
188
   * List the event collections.
189
   *
190
   * @return \MongoDB\Collection[]
191
   *   The collections with a name matching the event pattern.
192
   */
193
  public function eventCollections() {
194
    echo static::EVENT_COLLECTIONS_PATTERN;
195
    $options = [
196
      'filter' => [
197
        'name' => ['$regex' => static::EVENT_COLLECTIONS_PATTERN],
198
      ],
199
    ];
200
    $result = iterator_to_array($this->database->listCollections($options));
201
    return $result;
202
  }
203
204
  /**
205
   * Return a collection, given its template id.
206
   *
207
   * @param string $template_id
208
   *   The string representation of a template \MongoId.
209
   *
210
   * @return \MongoDB\Collection
211
   *   A collection object for the specified template id.
212
   */
213
  public function eventCollection($template_id) {
214
    $collection_name = static::EVENT_COLLECTION_PREFIX . $template_id;
215
    if (!preg_match('/' . static::EVENT_COLLECTIONS_PATTERN . '/', $collection_name)) {
216
      throw new InvalidArgumentException(t('Invalid watchdog template id `@id`.', [
217
        '@id' => $collection_name,
218
      ]));
219
    }
220
    $collection = $this->database->selectCollection($collection_name);
221
    return $collection;
222
  }
223
224
  /**
225
   * Ensure indexes are set on the collections.
226
   *
227
   * First index is on <line, timestamp> instead of <function, line, timestamp>,
228
   * because we write to this collection a lot, and the smaller index on two
229
   * numbers should be much faster to create than one with a string included.
230
   */
231
  public function ensureIndexes() {
232
    $templates = $this->database->selectCollection(static::TEMPLATE_COLLECTION);
233
    $indexes = [
234
      // Index for adding/updating increments.
235
      [
236
        'name' => 'for-increments',
237
        'key' => ['line' => 1, 'timestamp' => -1],
238
      ],
239
240
      // Index for admin page without filters.
241
      [
242
        'name' => 'admin-no-filters',
243
        'key' => ['timestamp' => -1],
244
      ],
245
246
      // Index for admin page filtering by type.
247
      [
248
        'name' => 'admin-by-type',
249
        'key' => ['type' => 1, 'timestamp' => -1],
250
      ],
251
252
      // Index for admin page filtering by severity.
253
      [
254
        'name' => 'admin-by-severity',
255
        'key' => ['severity' => 1, 'timestamp' => -1],
256
      ],
257
258
      // Index for admin page filtering by type and severity.
259
      [
260
        'name' => 'admin-by-both',
261
        'key' => ['type' => 1, 'severity' => 1, 'timestamp' => -1],
262
      ],
263
    ];
264
    $templates->createIndexes($indexes);
265
  }
266
267
  /**
268
   * Return the event templates collection.
269
   *
270
   * @return \MongoDB\Collection
271
   *   The collection.
272
   */
273
  public function templateCollection() {
274
    return $this->database->selectCollection(static::TEMPLATE_COLLECTION);
275
  }
276
277
}
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...
278