|
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) { |
|
|
|
|
|
|
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
|
|
|
} |
|
|
|
|
|
|
278
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.jsonfile (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.jsonto 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
requireorrequire-devsection?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceofchecks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.