1
0

BxDolLoggerDb.php 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. <?php
  2. declare(strict_types=1);
  3. use Psr\Log\AbstractLogger;
  4. use Psr\Log\LogLevel;
  5. final class BxDolLoggerDb extends AbstractLogger
  6. {
  7. private const LEVELS = [
  8. LogLevel::DEBUG => 0,
  9. LogLevel::INFO => 1,
  10. LogLevel::NOTICE => 2,
  11. LogLevel::WARNING => 3,
  12. LogLevel::ERROR => 4,
  13. LogLevel::CRITICAL => 5,
  14. LogLevel::ALERT => 6,
  15. LogLevel::EMERGENCY => 7,
  16. ];
  17. private \PDO $pdo;
  18. private \PDOStatement $stmt;
  19. private int $minimumLevel;
  20. private string $channel;
  21. public function __construct(string $channel = 'system')
  22. {
  23. $this->channel = $channel;
  24. $this->minimumLevel = self::LEVELS[LogLevel::INFO];
  25. $this->pdo = BxDolDb::getInstance()->getLink();
  26. $this->stmt = $this->pdo->prepare(
  27. 'INSERT INTO `sys_logger` (level, message, context, channel, created_at)
  28. VALUES (:level, :message, :context, :channel, :created_at)'
  29. );
  30. }
  31. public function log(mixed $level, string|Stringable $message, array $context = []): void
  32. {
  33. $level = (string) $level;
  34. if (!isset(self::LEVELS[$level])) {
  35. throw new \Psr\Log\InvalidArgumentException("Unknown log level: {$level}");
  36. }
  37. if (self::LEVELS[$level] < $this->minimumLevel) {
  38. return;
  39. }
  40. try {
  41. $this->stmt->execute([
  42. ':level' => $level,
  43. ':message' => $this->interpolate((string) $message, $context),
  44. ':context' => $this->contextToString($context),
  45. ':channel' => $this->channel,
  46. ':created_at' => (new \DateTimeImmutable())->format('Y-m-d H:i:s.v'),
  47. ]);
  48. } catch (Throwable) {
  49. // Swallow — logging must never crash the application
  50. }
  51. }
  52. private function contextToString(array $context): ?string
  53. {
  54. if (empty($context)) {
  55. return null;
  56. }
  57. $parts = [];
  58. if (isset($context['exception']) && $context['exception'] instanceof Throwable) {
  59. $e = $context['exception'];
  60. $parts[] = sprintf(
  61. "[exception] %s(%d): %s\nFile: %s:%d\nTrace:\n%s",
  62. $e::class, $e->getCode(), $e->getMessage(),
  63. $e->getFile(), $e->getLine(), $e->getTraceAsString()
  64. );
  65. unset($context['exception']);
  66. }
  67. foreach ($context as $key => $value) {
  68. $parts[] = sprintf('[%s] %s', $key, match (true) {
  69. is_null($value) => 'null',
  70. is_bool($value) => $value ? 'true' : 'false',
  71. is_scalar($value) => (string) $value,
  72. $value instanceof Stringable => (string) $value,
  73. is_array($value) || is_object($value) => json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  74. default => gettype($value),
  75. });
  76. }
  77. return implode("\n", $parts);
  78. }
  79. private function interpolate(string $message, array $context): string
  80. {
  81. if (!str_contains($message, '{')) {
  82. return $message;
  83. }
  84. $replace = [];
  85. foreach ($context as $key => $value) {
  86. if (!is_array($value) && (!is_object($value) || method_exists($value, '__toString'))) {
  87. $replace['{' . $key . '}'] = (string) $value;
  88. }
  89. }
  90. return strtr($message, $replace);
  91. }
  92. }