From 9094e8c061ec6e8fbd8163a66bdd5bb72061f94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 25 Apr 2025 08:31:45 +0200 Subject: [PATCH] feat: log to database --- config/packages/monolog.yaml | 8 +++ migrations/Version20250425063035.php | 35 +++++++++ src/BusProNet/ApiClient.php | 1 + src/Entity/LogEntry.php | 102 +++++++++++++++++++++++++++ src/Logger/DatabaseHandler.php | 46 ++++++++++++ src/Logger/RequestInfoProcessor.php | 27 +++++++ src/Logger/UserDataProcessor.php | 69 ++++++++++++++++++ 7 files changed, 288 insertions(+) create mode 100644 migrations/Version20250425063035.php create mode 100644 src/Entity/LogEntry.php create mode 100644 src/Logger/DatabaseHandler.php create mode 100644 src/Logger/RequestInfoProcessor.php create mode 100644 src/Logger/UserDataProcessor.php diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index c756188..9a2f4d8 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -7,6 +7,10 @@ monolog: when@dev: monolog: handlers: + database: + type: service + id: App\Logger\DatabaseHandler + channels: ["core", "bpn"] bpn: type: stream path: "%kernel.logs_dir%/%kernel.environment%.bpn.log" @@ -52,6 +56,10 @@ when@test: when@prod: monolog: handlers: + database: + type: service + id: App\Logger\DatabaseHandler + channels: ["core", "bpn"] bpn: type: rotating_file max_files: 7 diff --git a/migrations/Version20250425063035.php b/migrations/Version20250425063035.php new file mode 100644 index 0000000..edce393 --- /dev/null +++ b/migrations/Version20250425063035.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + CREATE TABLE core_log_entry (id INT AUTO_INCREMENT NOT NULL, channel VARCHAR(255) NOT NULL, message VARCHAR(255) NOT NULL, context JSON DEFAULT NULL COMMENT '(DC2Type:json)', extra JSON DEFAULT NULL COMMENT '(DC2Type:json)', created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + DROP TABLE core_log_entry + SQL); + } +} diff --git a/src/BusProNet/ApiClient.php b/src/BusProNet/ApiClient.php index f6e1a1e..9548b78 100644 --- a/src/BusProNet/ApiClient.php +++ b/src/BusProNet/ApiClient.php @@ -296,6 +296,7 @@ class ApiClient $this->logger->info('Sending request to BPN API', [ 'requestId' => $requestId, + 'type' => $data['satz']['@typ'], ]); if (true === $debug || true === $this->config['debug']) { diff --git a/src/Entity/LogEntry.php b/src/Entity/LogEntry.php new file mode 100644 index 0000000..2b6d368 --- /dev/null +++ b/src/Entity/LogEntry.php @@ -0,0 +1,102 @@ +channel = $channel; + $this->message = $message; + $this->createdAt = new \DateTimeImmutable('now'); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getMessage(): ?string + { + return $this->message; + } + + public function setMessage(string $message): self + { + $this->message = $message; + + return $this; + } + + public function getChannel(): ?string + { + return $this->channel; + } + + public function setChannel(?string $channel): self + { + $this->channel = $channel; + + return $this; + } + + public function getContext(): ?array + { + return $this->context; + } + + public function setContext(?array $context): self + { + $this->context = $context; + + return $this; + } + + public function getExtra(): ?array + { + return $this->extra; + } + + public function setExtra(?array $extra): self + { + $this->extra = $extra; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } +} diff --git a/src/Logger/DatabaseHandler.php b/src/Logger/DatabaseHandler.php new file mode 100644 index 0000000..a8bf7b8 --- /dev/null +++ b/src/Logger/DatabaseHandler.php @@ -0,0 +1,46 @@ +replacePlaceHolder($record); + + $logEntry = new LogEntry($record->channel, $message); + $logEntry + ->setContext($record->context) + ->setExtra($record->extra) + ; + + $this->entityManager->persist($logEntry); + $this->entityManager->flush(); + } + + private function replacePlaceHolder(LogRecord $record): string + { + $message = $record->message; + + if (!str_contains($message, '{')) { + return $message; + } + + $replacements = []; + foreach ($record->context as $k => $v) { + $replacements['{'.$k.'}'] = $v; + } + + return strtr($message, $replacements); + } +} diff --git a/src/Logger/RequestInfoProcessor.php b/src/Logger/RequestInfoProcessor.php new file mode 100644 index 0000000..cacc11e --- /dev/null +++ b/src/Logger/RequestInfoProcessor.php @@ -0,0 +1,27 @@ +requestStack->getMainRequest()) { + return $record; + } + + $record['extra']['uri'] = $request->getRequestUri(); + $record['extra']['method'] = $request->getMethod(); + + return $record; + } +} diff --git a/src/Logger/UserDataProcessor.php b/src/Logger/UserDataProcessor.php new file mode 100644 index 0000000..e8a05fc --- /dev/null +++ b/src/Logger/UserDataProcessor.php @@ -0,0 +1,69 @@ +extra['user'])) { + return $record; + } + + if ('cli' === PHP_SAPI) { + $record->extra['user'] = [ + 'id' => null, + 'username' => 'SYSTEM', + 'role' => 'system', + ]; + + return $record; + } + + /** @var User $user */ + $user = $this->security->getUser(); + + if (null === $user) { + $record->extra['user'] = [ + 'id' => null, + 'username' => 'ANONYMOUS', + 'role' => 'public', + ]; + + return $record; + } + + // Check for switched or aliased users + $originalUser = null; + $token = $this->security->getToken(); + if ($token instanceof SwitchUserToken) { + // User is currently switched to + $originalUser = $token->getOriginalToken()->getUser(); + } + + if (null !== $originalUser) { + $username = sprintf('%s (via %s)', $user->getUserIdentifier(), $originalUser->getUserIdentifier()); + } else { + $username = $user->getUserIdentifier(); + } + + $record->extra['user'] = [ + 'id' => $user->getId(), + 'username' => $username, + 'roles' => $user->getRoles(), + ]; + + return $record; + } +}