Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
96.49% |
55 / 57 |
|
33.33% |
1 / 3 |
CRAP | |
0.00% |
0 / 1 |
| SqlApiLoggerRepository | |
96.43% |
54 / 56 |
|
33.33% |
1 / 3 |
12 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| logRequest | |
97.62% |
41 / 42 |
|
0.00% |
0 / 1 |
9 | |||
| purgeExpiredLogs | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
2.00 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Core\Api\Infrastructure\Repository; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | use App\Core\Api\Domain\Model\ApiLogDetail; |
| 12 | use App\Core\Api\Domain\Model\ApiLogEntry; |
| 13 | use App\Core\Api\Domain\Repository\ApiLoggerInterface; |
| 14 | use PDO; |
| 15 | use Throwable; |
| 16 | |
| 17 | /** |
| 18 | * SQL Implementation of API Request Logger Repository. |
| 19 | * |
| 20 | * Persists high-throughput API access logs and detailed payload dumps using prepared PDO statements. |
| 21 | * |
| 22 | * @package App\Core\Api\Infrastructure\Repository |
| 23 | */ |
| 24 | final readonly class SqlApiLoggerRepository implements ApiLoggerInterface |
| 25 | { |
| 26 | /** |
| 27 | * SqlApiLoggerRepository constructor. |
| 28 | * |
| 29 | * @param PDO $pdo Active PDO database connection. |
| 30 | */ |
| 31 | public function __construct( |
| 32 | private PDO $pdo, |
| 33 | ) { |
| 34 | } |
| 35 | |
| 36 | /** {@inheritdoc} */ |
| 37 | public function logRequest(ApiLogEntry $entry, ?ApiLogDetail $detail = null): void |
| 38 | { |
| 39 | try { |
| 40 | $sql = 'INSERT INTO `a_logs_api_requests_records` ( |
| 41 | `request_id`, `client_instance_id`, `user_id`, `api_key_id`, |
| 42 | `http_method`, `route_pattern`, `request_uri`, `status_code`, |
| 43 | `duration_ms`, `memory_peak_bytes`, `response_bytes`, |
| 44 | `ip_address`, `user_agent`, `log_level`, |
| 45 | `is_slow`, `is_error`, `is_aborted`, `has_payload_dump`, `created_at` |
| 46 | ) VALUES ( |
| 47 | :request_id, :client_instance_id, :user_id, :api_key_id, |
| 48 | :http_method, :route_pattern, :request_uri, :status_code, |
| 49 | :duration_ms, :memory_peak_bytes, :response_bytes, |
| 50 | :ip_address, :user_agent, :log_level, |
| 51 | :is_slow, :is_error, :is_aborted, :has_payload_dump, :created_at |
| 52 | )'; |
| 53 | |
| 54 | $stmt = $this->pdo->prepare($sql); |
| 55 | $stmt->execute([ |
| 56 | ':request_id' => $entry->requestId, |
| 57 | ':client_instance_id' => $entry->clientInstanceId, |
| 58 | ':user_id' => $entry->userId, |
| 59 | ':api_key_id' => $entry->apiKeyId, |
| 60 | ':http_method' => $entry->httpMethod, |
| 61 | ':route_pattern' => $entry->routePattern, |
| 62 | ':request_uri' => substr($entry->requestUri, 0, 500), |
| 63 | ':status_code' => $entry->statusCode, |
| 64 | ':duration_ms' => number_format($entry->durationMs, 3, '.', ''), |
| 65 | ':memory_peak_bytes' => $entry->memoryPeakBytes, |
| 66 | ':response_bytes' => $entry->responseBytes, |
| 67 | ':ip_address' => $entry->ipAddress, |
| 68 | ':user_agent' => substr($entry->userAgent, 0, 255), |
| 69 | ':log_level' => $entry->logLevel, |
| 70 | ':is_slow' => $entry->isSlow ? 1 : 0, |
| 71 | ':is_error' => $entry->isError ? 1 : 0, |
| 72 | ':is_aborted' => $entry->isAborted ? 1 : 0, |
| 73 | ':has_payload_dump' => ($detail !== null || $entry->hasPayloadDump) ? 1 : 0, |
| 74 | ':created_at' => $entry->createdAt->format('Y-m-d H:i:s.v'), |
| 75 | ]); |
| 76 | |
| 77 | $logId = (int) $this->pdo->lastInsertId(); |
| 78 | |
| 79 | if ($detail !== null && $logId > 0) { |
| 80 | $detailSql = 'INSERT INTO `a_logs_api_details_records` ( |
| 81 | `id`, `request_id`, `request_headers`, `request_payload`, |
| 82 | `response_headers`, `response_payload`, |
| 83 | `exception_class`, `exception_message`, `exception_trace`, `created_at` |
| 84 | ) VALUES ( |
| 85 | :id, :request_id, :request_headers, :request_payload, |
| 86 | :response_headers, :response_payload, |
| 87 | :exception_class, :exception_message, :exception_trace, :created_at |
| 88 | )'; |
| 89 | |
| 90 | $detailStmt = $this->pdo->prepare($detailSql); |
| 91 | $detailStmt->execute([ |
| 92 | ':id' => $logId, |
| 93 | ':request_id' => $detail->requestId, |
| 94 | ':request_headers' => $detail->requestHeaders, |
| 95 | ':request_payload' => $detail->requestPayload, |
| 96 | ':response_headers' => $detail->responseHeaders, |
| 97 | ':response_payload' => $detail->responsePayload, |
| 98 | ':exception_class' => $detail->exceptionClass, |
| 99 | ':exception_message' => $detail->exceptionMessage, |
| 100 | ':exception_trace' => $detail->exceptionTrace, |
| 101 | ':created_at' => $entry->createdAt->format('Y-m-d H:i:s.v'), |
| 102 | ]); |
| 103 | } |
| 104 | } catch (Throwable) { |
| 105 | // Fail-safe: Logging failure must never break active user request |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | /** {@inheritdoc} */ |
| 110 | public function purgeExpiredLogs(int $standardRetentionDays = 14, int $errorRetentionDays = 90): int |
| 111 | { |
| 112 | $deleted = 0; |
| 113 | |
| 114 | try { |
| 115 | // 1. Purge fast standard success logs older than standard retention days |
| 116 | $sql1 = 'DELETE FROM `a_logs_api_requests_records` |
| 117 | WHERE `is_slow` = 0 |
| 118 | AND `is_error` = 0 |
| 119 | AND `is_aborted` = 0 |
| 120 | AND `created_at` < DATE_SUB(NOW(), INTERVAL :std_days DAY)'; |
| 121 | $stmt1 = $this->pdo->prepare($sql1); |
| 122 | $stmt1->execute([':std_days' => max(1, $standardRetentionDays)]); |
| 123 | $deleted += $stmt1->rowCount(); |
| 124 | |
| 125 | // 2. Purge remaining error/slow logs older than error retention days |
| 126 | $sql2 = 'DELETE FROM `a_logs_api_requests_records` |
| 127 | WHERE `created_at` < DATE_SUB(NOW(), INTERVAL :err_days DAY)'; |
| 128 | $stmt2 = $this->pdo->prepare($sql2); |
| 129 | $stmt2->execute([':err_days' => max(1, $errorRetentionDays)]); |
| 130 | $deleted += $stmt2->rowCount(); |
| 131 | } catch (Throwable) { |
| 132 | // Log error during maintenance |
| 133 | } |
| 134 | |
| 135 | return $deleted; |
| 136 | } |
| 137 | } |