Skip to content

Commit c2ca770

Browse files
committed
Improved LoggerUtility. Improvements in Sync Scripts
1 parent 4bdbf32 commit c2ca770

File tree

11 files changed

+1059
-575
lines changed

11 files changed

+1059
-575
lines changed

app/classes/Utilities/LoggerUtility.php

Lines changed: 280 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@
77
use Monolog\Logger;
88
use Monolog\Handler\StreamHandler;
99
use Monolog\Handler\RotatingFileHandler;
10+
use Monolog\Handler\ErrorLogHandler;
1011

1112
final class LoggerUtility
1213
{
1314
private static ?Logger $logger = null;
1415
private const LOG_FILENAME = 'logfile.log';
1516
private const LOG_ROTATIONS = 30;
17+
private const MAX_FILE_SIZE_MB = 100; // Maximum size per log file in MB
18+
private const MAX_TOTAL_LOG_SIZE_MB = 1000; // Maximum total size for all logs in MB
19+
private const MAX_MESSAGE_LENGTH = 10000; // Maximum length for a single log message
20+
private static int $logCallCount = 0;
21+
private static int $maxLogsPerRequest = 10000; // Prevent infinite logging loops
22+
private static bool $hasLoggedFallback = false; // Prevent recursive fallback logging
1623

1724
public static function getLogger(): Logger
1825
{
@@ -25,11 +32,34 @@ public static function getLogger(): Logger
2532
$logLevel = defined('LOG_LEVEL') ? self::parseLogLevel(LOG_LEVEL) : Level::Debug;
2633

2734
try {
35+
// Check total log directory size before proceeding
36+
if (is_dir($logDir) && !self::checkLogDirectorySize($logDir)) {
37+
self::useFallbackHandler("Log directory exceeded maximum size limit");
38+
return self::$logger;
39+
}
40+
2841
if (MiscUtility::makeDirectory($logDir, 0775)) {
2942
if (is_writable($logDir)) {
3043
$logPath = $logDir . '/' . self::LOG_FILENAME;
31-
$handler = new RotatingFileHandler($logPath, self::LOG_ROTATIONS, $logLevel);
44+
45+
// Check if current log file is too large
46+
if (file_exists($logPath) && filesize($logPath) > self::MAX_FILE_SIZE_MB * 1024 * 1024) {
47+
// Force rotation by renaming the file
48+
$backupName = $logDir . '/' . date('Y-m-d') . '-' . self::LOG_FILENAME;
49+
@rename($logPath, $backupName);
50+
}
51+
52+
// Use RotatingFileHandler
53+
$handler = new RotatingFileHandler(
54+
$logPath,
55+
self::LOG_ROTATIONS,
56+
$logLevel,
57+
true,
58+
0664,
59+
false
60+
);
3261
$handler->setFilenameFormat('{date}-{filename}', 'Y-m-d');
62+
3363
self::$logger->pushHandler($handler);
3464
} else {
3565
self::useFallbackHandler("Log directory not writable: $logDir");
@@ -38,17 +68,130 @@ public static function getLogger(): Logger
3868
self::useFallbackHandler("Failed to create log directory: $logDir");
3969
}
4070
} catch (Throwable $e) {
41-
self::useFallbackHandler($e->getMessage());
71+
self::useFallbackHandler("Exception during logger setup: " . $e->getMessage());
72+
}
73+
74+
// CRITICAL: Ensure we always have at least one handler to prevent crashes
75+
if (empty(self::$logger->getHandlers())) {
76+
self::useErrorLogHandler("No handlers were successfully configured - falling back to PHP error_log");
4277
}
4378

4479
return self::$logger;
4580
}
4681

82+
private static function checkLogDirectorySize(string $logDir): bool
83+
{
84+
try {
85+
$totalSize = 0;
86+
$maxSize = self::MAX_TOTAL_LOG_SIZE_MB * 1024 * 1024;
87+
88+
$iterator = new \RecursiveIteratorIterator(
89+
new \RecursiveDirectoryIterator($logDir, \FilesystemIterator::SKIP_DOTS),
90+
\RecursiveIteratorIterator::SELF_FIRST
91+
);
92+
93+
foreach ($iterator as $file) {
94+
if ($file->isFile()) {
95+
$totalSize += $file->getSize();
96+
97+
// Early exit if we've exceeded the limit
98+
if ($totalSize > $maxSize) {
99+
self::logToPhpErrorLog("WARNING: Log directory size exceeded {$maxSize} bytes. Cleaning up old logs...");
100+
self::cleanupOldLogs($logDir);
101+
return false;
102+
}
103+
}
104+
}
105+
106+
return true;
107+
} catch (Throwable $e) {
108+
self::logToPhpErrorLog("Failed to check log directory size: {$e->getMessage()}");
109+
return true; // Allow logging to continue on error
110+
}
111+
}
112+
113+
private static function cleanupOldLogs(string $logDir): void
114+
{
115+
try {
116+
$files = [];
117+
$iterator = new \RecursiveIteratorIterator(
118+
new \RecursiveDirectoryIterator($logDir, \FilesystemIterator::SKIP_DOTS)
119+
);
120+
121+
foreach ($iterator as $file) {
122+
if ($file->isFile() && preg_match('/\.log$/', $file->getFilename())) {
123+
$files[] = [
124+
'path' => $file->getRealPath(),
125+
'mtime' => $file->getMTime(),
126+
'size' => $file->getSize()
127+
];
128+
}
129+
}
130+
131+
// Sort by modification time (oldest first)
132+
usort($files, fn($a, $b) => $a['mtime'] <=> $b['mtime']);
133+
134+
// Delete oldest files until we're under 80% of the limit
135+
$targetSize = self::MAX_TOTAL_LOG_SIZE_MB * 1024 * 1024 * 0.8;
136+
$currentSize = array_sum(array_column($files, 'size'));
137+
138+
foreach ($files as $file) {
139+
if ($currentSize <= $targetSize) {
140+
break;
141+
}
142+
143+
if (@unlink($file['path'])) {
144+
$currentSize -= $file['size'];
145+
self::logToPhpErrorLog("Deleted old log file: {$file['path']}");
146+
}
147+
}
148+
} catch (Throwable $e) {
149+
self::logToPhpErrorLog("Failed to cleanup old logs: {$e->getMessage()}");
150+
}
151+
}
152+
47153
private static function useFallbackHandler(string $reason): void
48154
{
49-
$fallbackHandler = new StreamHandler('php://stderr', Level::Warning);
50-
self::$logger->pushHandler($fallbackHandler);
51-
error_log("LoggerUtility fallback: {$reason} | PHP error_log: " . self::getPhpErrorLogPath());
155+
try {
156+
// Try to use stderr as fallback
157+
$fallbackHandler = new StreamHandler('php://stderr', Level::Warning);
158+
self::$logger->pushHandler($fallbackHandler);
159+
self::logToPhpErrorLog("LoggerUtility fallback to stderr: {$reason}");
160+
} catch (Throwable $e) {
161+
// If even stderr fails, use PHP's error_log
162+
self::useErrorLogHandler("Stderr fallback failed: {$e->getMessage()} | Original reason: {$reason}");
163+
}
164+
}
165+
166+
private static function useErrorLogHandler(string $reason): void
167+
{
168+
try {
169+
// ErrorLogHandler uses PHP's error_log() function
170+
$errorLogHandler = new ErrorLogHandler(ErrorLogHandler::OPERATING_SYSTEM, Level::Warning);
171+
self::$logger->pushHandler($errorLogHandler);
172+
self::logToPhpErrorLog("LoggerUtility using PHP error_log handler: {$reason}");
173+
} catch (Throwable $e) {
174+
// Last resort - nothing we can do, just exit gracefully
175+
self::logToPhpErrorLog("CRITICAL: All logging handlers failed: {$e->getMessage()} - logging disabled");
176+
}
177+
}
178+
179+
/**
180+
* Safe logging to PHP's error_log to avoid recursion
181+
*/
182+
private static function logToPhpErrorLog(string $message): void
183+
{
184+
if (self::$hasLoggedFallback) {
185+
return; // Prevent spam
186+
}
187+
188+
self::$hasLoggedFallback = true;
189+
@error_log("LoggerUtility: {$message} | PHP error_log: " . self::getPhpErrorLogPath());
190+
191+
// Reset flag after a moment to allow future errors
192+
register_shutdown_function(function () {
193+
self::$hasLoggedFallback = false;
194+
});
52195
}
53196

54197
public static function getPhpErrorLogPath(): string
@@ -68,40 +211,102 @@ private static function getCallerInfo(int $index = 1): array
68211
public static function log(Level|string $level, string $message, array $context = []): void
69212
{
70213
try {
214+
// Prevent infinite logging loops
215+
self::$logCallCount++;
216+
if (self::$logCallCount > self::$maxLogsPerRequest) {
217+
self::logToPhpErrorLog("WARNING: Maximum logs per request exceeded. Possible logging loop detected.");
218+
return;
219+
}
220+
221+
// Truncate message if too long
222+
if (strlen($message) > self::MAX_MESSAGE_LENGTH) {
223+
$message = substr($message, 0, self::MAX_MESSAGE_LENGTH) . '... [message truncated - exceeded ' . self::MAX_MESSAGE_LENGTH . ' chars]';
224+
}
225+
71226
$logger = self::getLogger();
72227
$callerInfo = self::getCallerInfo(1);
73228
$context['file'] ??= $callerInfo['file'];
74229
$context['line'] ??= $callerInfo['line'];
75230

76-
// Only sanitize in non-debug mode
77-
if (
78-
defined('LOG_LEVEL') && strtoupper(LOG_LEVEL) !== 'DEBUG' &&
79-
(defined('APPLICATION_ENV') && APPLICATION_ENV !== 'development')
80-
) {
81-
$maxContextLength = 1000;
82-
foreach ($context as $key => $value) {
83-
if (is_string($value) && strlen($value) > $maxContextLength) {
84-
$context[$key] = substr($value, 0, $maxContextLength) . '... [truncated]';
85-
}
231+
// Sanitize context data
232+
$context = self::sanitizeContext($context);
86233

87-
if ($key === 'trace') {
88-
if (is_string($value) && strlen($value) > $maxContextLength) {
89-
$context[$key] = substr($value, 0, $maxContextLength) . '... [truncated]';
90-
} elseif (is_array($value)) {
91-
$context[$key] = array_slice($value, 0, 10);
92-
}
93-
}
234+
$logger->log($level, MiscUtility::toUtf8($message), $context);
235+
} catch (Throwable $e) {
236+
// CRITICAL: Never let logging crash the application
237+
self::logToPhpErrorLog("LoggerUtility::log() failed: {$e->getMessage()} | Original message: " . substr($message, 0, 200));
238+
}
239+
}
94240

95-
if (is_object($value) || is_resource($value)) {
96-
$context[$key] = '[omitted: ' . gettype($value) . ']';
97-
}
241+
private static function sanitizeContext(array $context): array
242+
{
243+
$maxContextLength = 1000;
244+
$maxArrayDepth = 5;
245+
246+
// Always sanitize, regardless of environment (safer default)
247+
$sanitized = [];
248+
249+
foreach ($context as $key => $value) {
250+
// Skip if key is too long
251+
if (is_string($key) && strlen($key) > 100) {
252+
continue;
253+
}
254+
255+
// Handle different types
256+
if (is_string($value)) {
257+
if (strlen($value) > $maxContextLength) {
258+
$sanitized[$key] = substr($value, 0, $maxContextLength) . '... [truncated]';
259+
} else {
260+
$sanitized[$key] = $value;
98261
}
262+
} elseif (is_array($value)) {
263+
if ($key === 'trace') {
264+
// Limit trace depth
265+
$sanitized[$key] = array_slice($value, 0, 10);
266+
} else {
267+
// Recursively sanitize arrays but limit depth
268+
$sanitized[$key] = self::sanitizeArrayRecursive($value, 0, $maxArrayDepth);
269+
}
270+
} elseif (is_object($value) || is_resource($value)) {
271+
$sanitized[$key] = '[omitted: ' . gettype($value) . ']';
272+
} elseif (is_scalar($value) || is_null($value)) {
273+
$sanitized[$key] = $value;
274+
} else {
275+
$sanitized[$key] = '[omitted: ' . gettype($value) . ']';
99276
}
277+
}
100278

101-
$logger->log($level, MiscUtility::toUtf8($message), $context);
102-
} catch (Throwable $e) {
103-
error_log("LoggerUtility failed: {$e->getMessage()} | Original message: {$message}");
279+
return $sanitized;
280+
}
281+
282+
private static function sanitizeArrayRecursive(array $array, int $depth, int $maxDepth): array
283+
{
284+
if ($depth >= $maxDepth) {
285+
return ['[max depth reached]'];
286+
}
287+
288+
$sanitized = [];
289+
$count = 0;
290+
$maxItems = 50; // Limit array items
291+
292+
foreach ($array as $key => $value) {
293+
if ($count++ >= $maxItems) {
294+
$sanitized['...'] = '[truncated - max items reached]';
295+
break;
296+
}
297+
298+
if (is_array($value)) {
299+
$sanitized[$key] = self::sanitizeArrayRecursive($value, $depth + 1, $maxDepth);
300+
} elseif (is_string($value) && strlen($value) > 500) {
301+
$sanitized[$key] = substr($value, 0, 500) . '... [truncated]';
302+
} elseif (is_object($value) || is_resource($value)) {
303+
$sanitized[$key] = '[' . gettype($value) . ']';
304+
} else {
305+
$sanitized[$key] = $value;
306+
}
104307
}
308+
309+
return $sanitized;
105310
}
106311

107312
public static function logDebug(string $message, array $context = []): void
@@ -134,4 +339,51 @@ private static function parseLogLevel(string $level): Level
134339
default => Level::Debug
135340
};
136341
}
342+
343+
/**
344+
* Reset the log call counter (useful for testing or long-running processes)
345+
*/
346+
public static function resetLogCallCount(): void
347+
{
348+
self::$logCallCount = 0;
349+
}
350+
351+
/**
352+
* Get current log statistics
353+
*/
354+
public static function getLogStats(): array
355+
{
356+
$logDir = defined('LOG_PATH') ? LOG_PATH : ROOT_PATH . '/logs';
357+
$stats = [
358+
'log_calls_this_request' => self::$logCallCount,
359+
'log_directory' => $logDir,
360+
'total_size_mb' => 0,
361+
'file_count' => 0
362+
];
363+
364+
try {
365+
if (is_dir($logDir)) {
366+
$totalSize = 0;
367+
$fileCount = 0;
368+
369+
$iterator = new \RecursiveIteratorIterator(
370+
new \RecursiveDirectoryIterator($logDir, \FilesystemIterator::SKIP_DOTS)
371+
);
372+
373+
foreach ($iterator as $file) {
374+
if ($file->isFile()) {
375+
$totalSize += $file->getSize();
376+
$fileCount++;
377+
}
378+
}
379+
380+
$stats['total_size_mb'] = round($totalSize / (1024 * 1024), 2);
381+
$stats['file_count'] = $fileCount;
382+
}
383+
} catch (Throwable $e) {
384+
$stats['error'] = $e->getMessage();
385+
}
386+
387+
return $stats;
388+
}
137389
}

0 commit comments

Comments
 (0)