pradosoft /
prado
| 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
Loading history...
|
|||
| 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;
}
Loading history...
|
|||
| 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 |