log.php 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. <?php
  2. declare(strict_types=1);
  3. // JSONL logging with size-based rotation. Ported from the PSA order system
  4. // (includes/functions.php logError/logAccess/rotateLogFileIfNeeded).
  5. function manageErrorLogFile(): string
  6. {
  7. return manageLogsDir() . "error.log";
  8. }
  9. function manageAccessLogFile(): string
  10. {
  11. return manageLogsDir() . "access.log";
  12. }
  13. function manageRotateLogFileIfNeeded(string $file): void
  14. {
  15. $maxBytes = (int) MANAGE_LOG_MAX_BYTES;
  16. if ($maxBytes < 1 || !is_file($file)) {
  17. return;
  18. }
  19. $size = filesize($file);
  20. if ($size === false || $size < $maxBytes) {
  21. manageDeleteExpiredLogFiles($file);
  22. return;
  23. }
  24. $keepFiles = max(1, (int) MANAGE_LOG_KEEP_FILES);
  25. for ($i = $keepFiles; $i >= 1; $i--) {
  26. $source = $i === 1 ? $file : $file . "." . ($i - 1);
  27. $target = $file . "." . $i;
  28. if (!is_file($source)) {
  29. continue;
  30. }
  31. if ($i === $keepFiles && is_file($target)) {
  32. @unlink($target);
  33. }
  34. @rename($source, $target);
  35. }
  36. manageDeleteExpiredLogFiles($file);
  37. }
  38. function manageDeleteExpiredLogFiles(string $file): void
  39. {
  40. $maxAge = (int) MANAGE_LOG_MAX_AGE_SECONDS;
  41. if ($maxAge < 1) {
  42. return;
  43. }
  44. foreach (glob($file . ".*") ?: [] as $rotated) {
  45. $mtime = @filemtime($rotated);
  46. if ($mtime !== false && time() - $mtime > $maxAge) {
  47. @unlink($rotated);
  48. }
  49. }
  50. }
  51. function manageWriteLogEntry(string $file, array $entry): void
  52. {
  53. try {
  54. manageEnsureDirectory(dirname($file));
  55. } catch (Throwable $exception) {
  56. return;
  57. }
  58. manageRotateLogFileIfNeeded($file);
  59. $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  60. if ($line === false) {
  61. return;
  62. }
  63. @file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX);
  64. }
  65. function manageLogError(string $message, array $context = [], string $level = "ERROR"): void
  66. {
  67. manageWriteLogEntry(manageErrorLogFile(), [
  68. "timestamp" => date("Y-m-d H:i:s.u"),
  69. "level" => $level,
  70. "message" => $message,
  71. "context" => $context,
  72. "ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown",
  73. "user_agent" => substr((string) ($_SERVER["HTTP_USER_AGENT"] ?? "unknown"), 0, 300),
  74. "request_uri" => $_SERVER["REQUEST_URI"] ?? "unknown",
  75. ]);
  76. }
  77. function manageLogAccess(string $message, array $context = []): void
  78. {
  79. manageWriteLogEntry(manageAccessLogFile(), [
  80. "timestamp" => date("Y-m-d H:i:s.u"),
  81. "message" => $message,
  82. "context" => $context,
  83. "ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown",
  84. "request_method" => $_SERVER["REQUEST_METHOD"] ?? "unknown",
  85. "request_uri" => $_SERVER["REQUEST_URI"] ?? "unknown",
  86. ]);
  87. }
  88. // Plain-text append-only log for S3 diagnostics. Kept separate from the JSONL
  89. // logs so the raw provider error stays readable in the management UI.
  90. function manageLogS3(string $message, array $context = []): void
  91. {
  92. try {
  93. manageEnsureDirectory(dirname(manageS3LogFile()));
  94. } catch (Throwable $exception) {
  95. return;
  96. }
  97. $line = date(DATE_ATOM) . " " . $message;
  98. $encoded = @json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  99. if (is_string($encoded) && $encoded !== "[]") {
  100. $line .= " " . $encoded;
  101. }
  102. @file_put_contents(manageS3LogFile(), $line . PHP_EOL, FILE_APPEND | LOCK_EX);
  103. }
  104. function manageLogS3Tail(int $lines): array
  105. {
  106. $file = manageS3LogFile();
  107. if (!is_file($file) || $lines < 1) {
  108. return [];
  109. }
  110. $content = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  111. if (!is_array($content)) {
  112. return [];
  113. }
  114. return array_slice($content, -$lines);
  115. }