1 | <?php |
||
2 | |||
3 | /** |
||
4 | * TLogRouter, TLogRoute, TFileLogRoute, TEmailLogRoute class file |
||
5 | * |
||
6 | * @author Qiang Xue <[email protected]> |
||
7 | * @link https://github.com/pradosoft/prado |
||
8 | * @license https://github.com/pradosoft/prado/blob/master/LICENSE |
||
9 | */ |
||
10 | |||
11 | namespace Prado\Util; |
||
12 | |||
13 | use Exception; |
||
14 | use Prado\Data\TDataSourceConfig; |
||
15 | use Prado\Data\TDbConnection; |
||
16 | use Prado\Exceptions\TConfigurationException; |
||
17 | use Prado\Exceptions\TLogException; |
||
18 | use Prado\TPropertyValue; |
||
19 | |||
20 | /** |
||
21 | * TDbLogRoute class |
||
22 | * |
||
23 | * TDbLogRoute stores log messages in a database table. |
||
24 | * To specify the database table, set {@see setConnectionID ConnectionID} to be |
||
25 | * the ID of a {@see \Prado\Data\TDataSourceConfig} module and {@see setLogTableName LogTableName}. |
||
26 | * If they are not setting, an SQLite3 database named 'sqlite3.log' will be created and used |
||
27 | * under the runtime directory. |
||
28 | * |
||
29 | * By default, the database table name is 'pradolog'. It has the following structure: |
||
30 | * ```sql |
||
31 | * CREATE TABLE pradolog |
||
32 | * ( |
||
33 | * log_id INTEGER NOT NULL PRIMARY KEY, |
||
34 | * level INTEGER, |
||
35 | * category VARCHAR(128), |
||
36 | * prefix VARCHAR(128), |
||
37 | * logtime VARCHAR(20), |
||
38 | * message VARCHAR(255) |
||
39 | * ); |
||
40 | * ``` |
||
41 | * |
||
42 | * 4.3.0 Notes: Add the `prefix` to the log table: |
||
43 | * `ALTER TABLE pradolog ADD COLUMN prefix VARCHAR(128) AFTER category;` |
||
44 | * |
||
45 | * @author Qiang Xue <[email protected]> |
||
46 | * @author Brad Anderson <[email protected]> |
||
47 | * @since 3.1.2 |
||
48 | */ |
||
49 | class TDbLogRoute extends TLogRoute |
||
50 | { |
||
51 | /** |
||
52 | * @var string the ID of TDataSourceConfig module |
||
53 | */ |
||
54 | private $_connID = ''; |
||
55 | /** |
||
56 | * @var TDbConnection the DB connection instance |
||
57 | */ |
||
58 | private $_db; |
||
59 | /** |
||
60 | * @var string name of the DB log table |
||
61 | */ |
||
62 | private $_logTable = 'pradolog'; |
||
63 | /** |
||
64 | * @var bool whether the log DB table should be created automatically |
||
65 | */ |
||
66 | private bool $_autoCreate = true; |
||
67 | /** |
||
68 | * @var ?float The number of seconds of the log to retain. Default null for logs are |
||
69 | * not deleted. |
||
70 | * @since 4.3.0 |
||
71 | */ |
||
72 | private ?float $_retainPeriod = null; |
||
73 | |||
74 | /** |
||
75 | * Destructor. |
||
76 | * Disconnect the db connection. |
||
77 | */ |
||
78 | public function __destruct() |
||
79 | { |
||
80 | if ($this->_db !== null) { |
||
81 | $this->_db->setActive(false); |
||
82 | } |
||
83 | parent::__destruct(); |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * Initializes this module. |
||
88 | * This method is required by the IModule interface. |
||
89 | * It initializes the database for logging purpose. |
||
90 | * @param \Prado\Xml\TXmlElement $config configuration for this module, can be null |
||
91 | * @throws TConfigurationException if the DB table does not exist. |
||
92 | */ |
||
93 | public function init($config) |
||
94 | { |
||
95 | $db = $this->getDbConnection(); |
||
96 | $db->setActive(true); |
||
97 | |||
98 | $sql = 'SELECT * FROM ' . $this->_logTable . ' WHERE 0=1'; |
||
99 | try { |
||
100 | $db->createCommand($sql)->query()->close(); |
||
101 | } catch (Exception $e) { |
||
102 | // DB table not exists |
||
103 | if ($this->_autoCreate) { |
||
104 | $this->createDbTable(); |
||
105 | } else { |
||
106 | throw new TConfigurationException('dblogroute_table_nonexistent', $this->_logTable); |
||
107 | } |
||
108 | } |
||
109 | |||
110 | parent::init($config); |
||
111 | } |
||
112 | |||
113 | /** |
||
114 | * Stores log messages into database. |
||
115 | * @param array $logs list of log messages |
||
116 | * @param bool $final is the final flush |
||
117 | * @param array $meta the meta data for the logs. |
||
118 | * @throws TLogException when the DB insert fails. |
||
119 | */ |
||
120 | protected function processLogs(array $logs, bool $final, array $meta) |
||
121 | { |
||
122 | $sql = 'INSERT INTO ' . $this->_logTable . '(level, category, prefix, logtime, message) VALUES (:level, :category, :prefix, :logtime, :message)'; |
||
123 | $command = $this->getDbConnection()->createCommand($sql); |
||
124 | foreach ($logs as $log) { |
||
125 | $command->bindValue(':message', (string) $log[TLogger::LOG_MESSAGE]); |
||
126 | $command->bindValue(':level', $log[TLogger::LOG_LEVEL]); |
||
127 | $command->bindValue(':category', $log[TLogger::LOG_CATEGORY]); |
||
128 | $command->bindValue(':prefix', $this->getLogPrefix($log)); |
||
129 | $command->bindValue(':logtime', sprintf('%F', $log[TLogger::LOG_TIME])); |
||
130 | if (!$command->execute()) { |
||
131 | throw new TLogException('dblogroute_insert_failed', $this->_logTable); |
||
132 | } |
||
133 | } |
||
134 | if (!empty($seconds = $this->getRetainPeriod())) { |
||
135 | $this->deleteDbLog(null, null, null, microtime(true) - $seconds); |
||
136 | } |
||
137 | } |
||
138 | |||
139 | /** |
||
140 | * Computes the where SQL clause based upon level, categories, minimum time and maximum time. |
||
141 | * @param ?int $level The bit mask of log levels to search for |
||
142 | * @param null|null|array|string $categories The categories to search for. Strings |
||
143 | * are exploded with ','. |
||
144 | * @param ?float $minTime All logs after this time are found |
||
145 | * @param ?float $maxTime All logs before this time are found |
||
146 | * @param mixed $values the values to fill in. |
||
147 | * @return string The where clause for the various SQL statements. |
||
148 | * @since 4.3.0 |
||
149 | */ |
||
150 | protected function getLogWhere(?int $level, null|string|array $categories, ?float $minTime, ?float $maxTime, &$values): string |
||
151 | { |
||
152 | $where = ''; |
||
153 | $values = []; |
||
154 | if ($level !== null) { |
||
155 | $where .= '((level & :level) > 0)'; |
||
156 | $values[':level'] = $level; |
||
157 | } |
||
158 | if ($categories !== null) { |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
159 | if (is_string($categories)) { |
||
0 ignored issues
–
show
|
|||
160 | $categories = array_map('trim', explode(',', $categories)); |
||
161 | } |
||
162 | $i = 0; |
||
163 | $or = ''; |
||
164 | foreach ($categories as $category) { |
||
165 | $c = $category[0] ?? 0; |
||
166 | if ($c === '!' || $c === '~') { |
||
167 | if ($where) { |
||
168 | $where .= ' AND '; |
||
169 | } |
||
170 | $category = substr($category, 1); |
||
171 | $where .= "(category NOT LIKE :category{$i})"; |
||
172 | } else { |
||
173 | if ($or) { |
||
174 | $or .= ' OR '; |
||
175 | } |
||
176 | $or .= "(category LIKE :category{$i})"; |
||
177 | } |
||
178 | $category = str_replace('*', '%', $category); |
||
179 | $values[':category' . ($i++)] = $category; |
||
180 | } |
||
181 | if ($or) { |
||
182 | if ($where) { |
||
183 | $where .= ' AND '; |
||
184 | } |
||
185 | $where .= '(' . $or . ')'; |
||
186 | } |
||
187 | } |
||
188 | if ($minTime !== null) { |
||
189 | if ($where) { |
||
190 | $where .= ' AND '; |
||
191 | } |
||
192 | $where .= 'logtime >= :mintime'; |
||
193 | $values[':mintime'] = sprintf('%F', $minTime); |
||
194 | } |
||
195 | if ($maxTime !== null) { |
||
196 | if ($where) { |
||
197 | $where .= ' AND '; |
||
198 | } |
||
199 | $where .= 'logtime < :maxtime'; |
||
200 | $values[':maxtime'] = sprintf('%F', $maxTime); |
||
201 | } |
||
202 | if ($where) { |
||
203 | $where = ' WHERE ' . $where; |
||
204 | } |
||
205 | return $where; |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * Gets the number of logs in the database fitting the provided criteria. |
||
210 | * @param ?int $level The bit mask of log levels to search for |
||
211 | * @param null|null|array|string $categories The categories to search for. Strings |
||
212 | * are exploded with ','. |
||
213 | * @param ?float $minTime All logs after this time are found |
||
214 | * @param ?float $maxTime All logs before this time are found |
||
215 | * @return string The where clause for the various SQL statements.. |
||
216 | * @since 4.3.0 |
||
217 | */ |
||
218 | public function getDBLogCount(?int $level = null, null|string|array $categories = null, ?float $minTime = null, ?float $maxTime = null) |
||
219 | { |
||
220 | $values = []; |
||
221 | $where = $this->getLogWhere($level, $categories, $minTime, $maxTime, $values); |
||
222 | $sql = 'SELECT COUNT(*) FROM ' . $this->_logTable . $where; |
||
223 | $command = $this->getDbConnection()->createCommand($sql); |
||
224 | foreach ($values as $key => $value) { |
||
225 | $command->bindValue($key, $value); |
||
226 | } |
||
227 | return $command->queryScalar(); |
||
228 | } |
||
229 | |||
230 | /** |
||
231 | * Gets the number of logs in the database fitting the provided criteria. |
||
232 | * @param ?int $level The bit mask of log levels to search for |
||
233 | * @param null|null|array|string $categories The categories to search for. Strings |
||
234 | * are exploded with ','. |
||
235 | * @param ?float $minTime All logs after this time are found |
||
236 | * @param ?float $maxTime All logs before this time are found |
||
237 | * @param string $order The order statement. |
||
238 | * @param string $limit The limit statement. |
||
239 | * @return \Prado\Data\TDbDataReader the logs from the database. |
||
240 | * @since 4.3.0 |
||
241 | */ |
||
242 | public function getDBLogs(?int $level = null, null|string|array $categories = null, ?float $minTime = null, ?float $maxTime = null, string $order = '', string $limit = '') |
||
243 | { |
||
244 | $values = []; |
||
245 | if ($order) { |
||
246 | $order .= ' ORDER BY ' . $order; |
||
247 | } |
||
248 | if ($limit) { |
||
249 | $limit .= ' LIMIT ' . $limit; |
||
250 | } |
||
251 | $where = $this->getLogWhere($level, $categories, $minTime, $maxTime, $values); |
||
252 | $sql = 'SELECT * FROM ' . $this->_logTable . $where . $order . $limit; |
||
253 | $command = $this->getDbConnection()->createCommand($sql); |
||
254 | foreach ($values as $key => $value) { |
||
255 | $command->bindValue($key, $value); |
||
256 | } |
||
257 | return $command->query(); |
||
258 | } |
||
259 | |||
260 | /** |
||
261 | * Deletes log items from the database that match the criteria. |
||
262 | * @param ?int $level The bit mask of log levels to search for |
||
263 | * @param null|null|array|string $categories The categories to search for. Strings |
||
264 | * are exploded with ','. |
||
265 | * @param ?float $minTime All logs after this time are found |
||
266 | * @param ?float $maxTime All logs before this time are found |
||
267 | * @return int the number of logs in the database. |
||
268 | * @since 4.3.0 |
||
269 | */ |
||
270 | public function deleteDBLog(?int $level = null, null|string|array $categories = null, ?float $minTime = null, ?float $maxTime = null) |
||
271 | { |
||
272 | $values = []; |
||
273 | $where = $this->getLogWhere($level, $categories, $minTime, $maxTime, $values); |
||
274 | $sql = 'DELETE FROM ' . $this->_logTable . $where; |
||
275 | $command = $this->getDbConnection()->createCommand($sql); |
||
276 | foreach ($values as $key => $value) { |
||
277 | $command->bindValue($key, $value); |
||
278 | } |
||
279 | return $command->execute(); |
||
280 | } |
||
281 | |||
282 | /** |
||
283 | * Creates the DB table for storing log messages. |
||
284 | */ |
||
285 | protected function createDbTable() |
||
286 | { |
||
287 | $db = $this->getDbConnection(); |
||
288 | $driver = $db->getDriverName(); |
||
289 | $autoidAttributes = ''; |
||
290 | if ($driver === 'mysql') { |
||
291 | $autoidAttributes = 'AUTO_INCREMENT'; |
||
292 | } |
||
293 | if ($driver === 'pgsql') { |
||
294 | $param = 'SERIAL'; |
||
295 | } else { |
||
296 | $param = 'INTEGER NOT NULL'; |
||
297 | } |
||
298 | |||
299 | $sql = 'CREATE TABLE ' . $this->_logTable . ' ( |
||
300 | log_id ' . $param . ' PRIMARY KEY ' . $autoidAttributes . ', |
||
301 | level INTEGER, |
||
302 | category VARCHAR(128), |
||
303 | prefix VARCHAR(128), |
||
304 | logtime VARCHAR(20), |
||
305 | message VARCHAR(255))'; |
||
306 | $db->createCommand($sql)->execute(); |
||
307 | } |
||
308 | |||
309 | /** |
||
310 | * Creates the DB connection. |
||
311 | * @throws TConfigurationException if module ID is invalid or empty |
||
312 | * @return \Prado\Data\TDbConnection the created DB connection |
||
313 | */ |
||
314 | protected function createDbConnection() |
||
315 | { |
||
316 | if ($this->_connID !== '') { |
||
317 | $config = $this->getApplication()->getModule($this->_connID); |
||
318 | if ($config instanceof TDataSourceConfig) { |
||
319 | return $config->getDbConnection(); |
||
320 | } else { |
||
321 | throw new TConfigurationException('dblogroute_connectionid_invalid', $this->_connID); |
||
322 | } |
||
323 | } else { |
||
324 | $db = new TDbConnection(); |
||
325 | // default to SQLite3 database |
||
326 | $dbFile = $this->getApplication()->getRuntimePath() . DIRECTORY_SEPARATOR . 'sqlite3.log'; |
||
327 | $db->setConnectionString('sqlite:' . $dbFile); |
||
328 | return $db; |
||
329 | } |
||
330 | } |
||
331 | |||
332 | /** |
||
333 | * @return \Prado\Data\TDbConnection the DB connection instance |
||
334 | */ |
||
335 | public function getDbConnection() |
||
336 | { |
||
337 | if ($this->_db === null) { |
||
338 | $this->_db = $this->createDbConnection(); |
||
339 | } |
||
340 | return $this->_db; |
||
341 | } |
||
342 | |||
343 | /** |
||
344 | * @return string the ID of a {@see \Prado\Data\TDataSourceConfig} module. Defaults to empty string, meaning not set. |
||
345 | */ |
||
346 | public function getConnectionID() |
||
347 | { |
||
348 | return $this->_connID; |
||
349 | } |
||
350 | |||
351 | /** |
||
352 | * Sets the ID of a TDataSourceConfig module. |
||
353 | * The datasource module will be used to establish the DB connection for this log route. |
||
354 | * @param string $value ID of the {@see \Prado\Data\TDataSourceConfig} module |
||
355 | * @return static The current object. |
||
356 | */ |
||
357 | public function setConnectionID($value): static |
||
358 | { |
||
359 | $this->_connID = $value; |
||
360 | |||
361 | return $this; |
||
362 | } |
||
363 | |||
364 | /** |
||
365 | * @return string the name of the DB table to store log content. Defaults to 'pradolog'. |
||
366 | * @see setAutoCreateLogTable |
||
367 | */ |
||
368 | public function getLogTableName() |
||
369 | { |
||
370 | return $this->_logTable; |
||
371 | } |
||
372 | |||
373 | /** |
||
374 | * Sets the name of the DB table to store log content. |
||
375 | * Note, if {@see setAutoCreateLogTable AutoCreateLogTable} is false |
||
376 | * and you want to create the DB table manually by yourself, |
||
377 | * you need to make sure the DB table is of the following structure: |
||
378 | * (key CHAR(128) PRIMARY KEY, value BLOB, expire INT) |
||
379 | * @param string $value the name of the DB table to store log content |
||
380 | * @return static The current object. |
||
381 | * @see setAutoCreateLogTable |
||
382 | */ |
||
383 | public function setLogTableName($value): static |
||
384 | { |
||
385 | $this->_logTable = $value; |
||
386 | |||
387 | return $this; |
||
388 | } |
||
389 | |||
390 | /** |
||
391 | * @return bool whether the log DB table should be automatically created if not exists. Defaults to true. |
||
392 | * @see setAutoCreateLogTable |
||
393 | */ |
||
394 | public function getAutoCreateLogTable() |
||
395 | { |
||
396 | return $this->_autoCreate; |
||
397 | } |
||
398 | |||
399 | /** |
||
400 | * @param bool $value whether the log DB table should be automatically created if not exists. |
||
401 | * @return static The current object. |
||
402 | * @see setLogTableName |
||
403 | */ |
||
404 | public function setAutoCreateLogTable($value): static |
||
405 | { |
||
406 | $this->_autoCreate = TPropertyValue::ensureBoolean($value); |
||
407 | |||
408 | return $this; |
||
409 | } |
||
410 | |||
411 | /** |
||
412 | * @return ?float The seconds to retain. Null is no end. |
||
413 | * @since 4.3.0 |
||
414 | */ |
||
415 | public function getRetainPeriod(): ?float |
||
416 | { |
||
417 | return $this->_retainPeriod; |
||
418 | } |
||
419 | |||
420 | /** |
||
421 | * @param null|int|string $value Number of seconds or "PT" period time. |
||
422 | * @throws TConfigurationException when the time span is not a valid "PT" string. |
||
423 | * @return static The current object. |
||
424 | * @since 4.3.0 |
||
425 | */ |
||
426 | public function setRetainPeriod($value): static |
||
427 | { |
||
428 | if (is_numeric($value)) { |
||
429 | $value = (float) $value; |
||
430 | if ($value === 0.0) { |
||
0 ignored issues
–
show
|
|||
431 | $value = null; |
||
432 | } |
||
433 | $this->_retainPeriod = $value; |
||
434 | return $this; |
||
435 | } |
||
436 | if (!($value = TPropertyValue::ensureString($value))) { |
||
437 | $value = null; |
||
438 | } |
||
439 | $seconds = false; |
||
440 | if ($value && ($seconds = static::timespanToSeconds($value)) === false) { |
||
441 | throw new TConfigurationException('dblogroute_bad_retain_period', $value); |
||
442 | } |
||
443 | |||
444 | $this->_retainPeriod = ($seconds !== false) ? $seconds : $value; |
||
0 ignored issues
–
show
It seems like
$seconds !== false ? $seconds : $value can also be of type string . However, the property $_retainPeriod is declared as type double|null . Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
![]() |
|||
445 | |||
446 | return $this; |
||
447 | } |
||
448 | |||
449 | /** |
||
450 | * @param string $timespan The time span to compute the number of seconds. |
||
451 | * @retutrn ?int the number of seconds of the time span. |
||
452 | * @since 4.3.0 |
||
453 | */ |
||
454 | public static function timespanToSeconds(string $timespan): ?int |
||
455 | { |
||
456 | if (($interval = new \DateInterval($timespan)) === false) { |
||
0 ignored issues
–
show
|
|||
457 | return null; |
||
458 | } |
||
459 | |||
460 | $datetime1 = new \DateTime(); |
||
461 | $datetime2 = clone $datetime1; |
||
462 | $datetime2->add($interval); |
||
463 | $diff = $datetime2->getTimestamp() - $datetime1->getTimestamp(); |
||
464 | return $diff; |
||
465 | } |
||
466 | |||
467 | } |
||
468 |