Bearsampp 2026.7.11
Loading...
Searching...
No Matches
class.log.php
Go to the documentation of this file.
1<?php
2/*
3 *
4 * * Copyright (c) 2022-2025 Bearsampp
5 * * License: GNU General Public License version 3 or later; see LICENSE.txt
6 * * Website: https://bearsampp.com
7 * * Github: https://github.com/Bearsampp
8 *
9 */
10
29class Log
30{
31 const ERROR = 'ERROR';
32 const WARNING = 'WARNING';
33 const INFO = 'INFO';
34 const DEBUG = 'DEBUG';
35 const TRACE = 'TRACE';
36
38 private static $logBuffer = [];
39
41 private static $logBufferSize = 50;
42
44 private static $shutdownRegistered = false;
45
47 private static $logStats = [
48 'buffered' => 0,
49 'flushed' => 0,
50 'writes' => 0,
51 'async' => 0,
52 ];
53
55 private static $asyncQueueDir = null;
56
58 private static $asyncEnabled = true;
59
61 private static $maxAsyncQueueBatch = 5;
62
72 public static function init()
73 {
74 if (!self::$shutdownRegistered) {
75 register_shutdown_function([__CLASS__, 'flush']);
76 register_shutdown_function([__CLASS__, 'processAsyncQueue']);
77 self::$shutdownRegistered = true;
78
79 // Initialize async queue directory
81
82 // Auto-disable async when TRACE logging is enabled (for live log monitoring)
84 }
85 }
86
94 private static function checkVerbosityAndAdjustAsync()
95 {
96 global $bearsamppConfig;
97
98 try {
99 if (!isset($bearsamppConfig)) {
100 return;
101 }
102
103 $verbosity = $bearsamppConfig->getLogsVerbose();
104
105 // Disable async and reduce buffer size for DEBUG/TRACE logging
106 // This ensures live log monitoring shows logs immediately
107 if ($verbosity === Config::VERBOSE_TRACE) {
108 self::$asyncEnabled = false;
109 // Use smaller buffer for TRACE to flush more frequently (every 10 entries)
110 self::$logBufferSize = 10;
111 } elseif ($verbosity === Config::VERBOSE_DEBUG) {
112 self::$asyncEnabled = false;
113 // For DEBUG level, use moderate buffer size (25 entries)
114 self::$logBufferSize = 25;
115 }
116 // For INFO and REPORT levels, use default buffer size (50) with async enabled
117 } catch (Exception $e) {
118 // Silently fail - this is just an optimization
119 }
120 }
121
127 private static function initializeAsyncQueue()
128 {
129 try {
130 // Set up queue directory in tmp
131 self::$asyncQueueDir = Path::getTmpPath() . '/log-queue';
132
133 // Create queue directory if it doesn't exist
134 if (!is_dir(self::$asyncQueueDir)) {
135 @mkdir(self::$asyncQueueDir, 0755, true);
136 }
137 } catch (Exception $e) {
138 self::$asyncEnabled = false;
139 }
140 }
141
154 private static function write($data, $type, $file = null)
155 {
157
158 // Safety check: if globals aren't initialised, fall back to error_log
159 if (!isset($bearsamppRoot) || !isset($bearsamppCore) || !isset($bearsamppConfig)) {
160 error_log('[' . $type . '] ' . $data);
161 return;
162 }
163
164 // Lazily register the shutdown handler if init() was not called explicitly
165 self::init();
166
167 // Resolve default file path only when the caller did not supply one
168 if ($file === null) {
169 $file = $type === self::ERROR
172
173 if (!$bearsamppRoot->isRoot()) {
175 }
176 }
177
178 $verbose = [];
179 $verbose[Config::VERBOSE_SIMPLE] = $type === self::ERROR || $type === self::WARNING;
180 $verbose[Config::VERBOSE_REPORT] = $verbose[Config::VERBOSE_SIMPLE] || $type === self::INFO;
181 $verbose[Config::VERBOSE_DEBUG] = $verbose[Config::VERBOSE_REPORT] || $type === self::DEBUG;
182 $verbose[Config::VERBOSE_TRACE] = $verbose[Config::VERBOSE_DEBUG] || $type === self::TRACE;
183
184 $writeLog = false;
185 if ($bearsamppConfig->getLogsVerbose() === Config::VERBOSE_SIMPLE && $verbose[Config::VERBOSE_SIMPLE]) {
186 $writeLog = true;
187 } elseif ($bearsamppConfig->getLogsVerbose() === Config::VERBOSE_REPORT && $verbose[Config::VERBOSE_REPORT]) {
188 $writeLog = true;
189 } elseif ($bearsamppConfig->getLogsVerbose() === Config::VERBOSE_DEBUG && $verbose[Config::VERBOSE_DEBUG]) {
190 $writeLog = true;
191 } elseif ($bearsamppConfig->getLogsVerbose() === Config::VERBOSE_TRACE && $verbose[Config::VERBOSE_TRACE]) {
192 $writeLog = true;
193 }
194
195 if ($writeLog) {
196 self::$logBuffer[] = [
197 'file' => $file,
198 'data' => $data,
199 'type' => $type,
200 'time' => time(),
201 ];
202 self::$logStats['buffered']++;
203
204 // Flush immediately for:
205 // 1. Errors (always)
206 // 2. TRACE/DEBUG level logs (for real-time visibility during debugging)
207 // 3. When buffer reaches the configured size limit
208 $debugVerbosity = $bearsamppConfig->getLogsVerbose();
209 $isDebugMode = ($debugVerbosity === Config::VERBOSE_TRACE || $debugVerbosity === Config::VERBOSE_DEBUG);
210 $shouldFlush = $type === self::ERROR ||
211 $isDebugMode ||
212 count(self::$logBuffer) >= self::$logBufferSize;
213
214 if ($shouldFlush) {
215 self::flush();
216 }
217 }
218 }
219
228 public static function flush()
229 {
230 if (empty(self::$logBuffer)) {
231 return;
232 }
233
235
236 // If the core global is gone (e.g. during an abnormal shutdown), fall back to error_log
237 if (!isset($bearsamppCore)) {
238 foreach (self::$logBuffer as $log) {
239 error_log('[' . date('Y-m-d H:i:s', $log['time']) . '] [' . $log['type'] . '] ' . $log['data']);
240 }
241 self::$logStats['flushed'] += count(self::$logBuffer);
242 self::$logBuffer = [];
243 return;
244 }
245
246 // Check if DEBUG or TRACE logging is active and force sync writes
247 $forceSync = false;
248 try {
249 if (isset($bearsamppConfig)) {
250 $verbosity = $bearsamppConfig->getLogsVerbose();
251 if ($verbosity === Config::VERBOSE_TRACE || $verbosity === Config::VERBOSE_DEBUG) {
252 $forceSync = true;
253 }
254 }
255 } catch (Exception $e) {
256 // Ignore errors checking config
257 }
258
259 // Group logs by destination file
260 $logsByFile = [];
261 foreach (self::$logBuffer as $log) {
262 $logsByFile[$log['file']][] = $log;
263 }
264
265 foreach ($logsByFile as $file => $logs) {
266 $content = '';
267 foreach ($logs as $log) {
268 $content .= '[' . date('Y-m-d H:i:s', $log['time']) . '] # ' .
269 APP_TITLE . ' ' . $bearsamppCore->getAppVersion() . ' # ' .
270 $log['type'] . ': ' . $log['data'] . PHP_EOL;
271 }
272
273 // Use sync writes if TRACE is enabled or async is disabled
274 if (!$forceSync && self::$asyncEnabled) {
275 // Queue for async processing (non-blocking)
276 $queued = self::queueAsyncWrite($file, $content);
277
278 if ($queued) {
279 // Successfully queued, content will be written in background
280 self::$logStats['async']++;
281 self::$logStats['writes']++;
282 continue;
283 }
284 // If async queueing failed, fall through to sync write
285 }
286
287 // Synchronous write (immediate for TRACE, fallback for others)
288 $written = @file_put_contents($file, $content, FILE_APPEND | LOCK_EX);
289 if ($written === false) {
290 // File write failed — ensure entries are not silently lost
291 foreach ($logs as $log) {
292 error_log('[' . $log['type'] . '] ' . $log['data'] . ' (target: ' . $file . ')');
293 }
294 }
295
296 self::$logStats['writes']++;
297 }
298
299 self::$logStats['flushed'] += count(self::$logBuffer);
300 self::$logBuffer = [];
301 }
302
311 private static function queueAsyncWrite($file, $content)
312 {
313 if (!self::$asyncEnabled || !is_dir(self::$asyncQueueDir)) {
314 return false;
315 }
316
317 try {
318 // Create a unique queue file for this write
319 $queueFile = self::$asyncQueueDir . '/' . uniqid('log_', true) . '.queue';
320
321 // Queue entry contains the target file and content
322 $queueEntry = [
323 'file' => $file,
324 'content' => $content,
325 'timestamp' => time(),
326 ];
327
328 // Write queue entry (this is a small, fast operation)
329 $serialized = serialize($queueEntry);
330 $written = @file_put_contents($queueFile, $serialized, LOCK_EX);
331
332 return ($written !== false);
333 } catch (Exception $e) {
334 // Silently fail - this is async so we don't want to interrupt main process
335 return false;
336 }
337 }
338
347 public static function flushAsyncQueue()
348 {
350 }
351
358 public static function processAsyncQueue()
359 {
360 if (!is_dir(self::$asyncQueueDir)) {
361 return 0;
362 }
363
364 $processed = 0;
365
366 try {
367 $queueFiles = glob(self::$asyncQueueDir . '/*.queue');
368
369 if (empty($queueFiles)) {
370 return 0;
371 }
372
373 // Group entries by target file
374 $entriesByFile = [];
375
376 foreach ($queueFiles as $queueFile) {
377 try {
378 // Read and deserialize queue entry
379 $serialized = @file_get_contents($queueFile);
380 if ($serialized === false) {
381 continue;
382 }
383
384 $entry = @unserialize($serialized, ['allowed_classes' => false]);
385 if ($entry === false || !isset($entry['file']) || !isset($entry['content'])) {
386 // Invalid queue entry, remove it
387 @unlink($queueFile);
388 continue;
389 }
390
391 // Group by target file
392 $targetFile = $entry['file'];
393 if (!isset($entriesByFile[$targetFile])) {
394 $entriesByFile[$targetFile] = [];
395 }
396
397 $entriesByFile[$targetFile][] = $entry['content'];
398 $processed++;
399
400 // Remove the processed queue file
401 @unlink($queueFile);
402
403 } catch (Exception $e) {
404 // Skip bad entries
405 @unlink($queueFile);
406 }
407 }
408
409 // Write all accumulated entries to their target files
410 foreach ($entriesByFile as $targetFile => $contents) {
411 try {
412 $combined = implode('', $contents);
413 @file_put_contents($targetFile, $combined, FILE_APPEND | LOCK_EX);
414 } catch (Exception $e) {
415 // Log write failed - use error_log as fallback
416 error_log('Failed to write to ' . $targetFile);
417 }
418 }
419
420 // Clean up any stale queue files
422
423 } catch (Exception $e) {
424 // Silently fail
425 }
426
427 return $processed;
428 }
429
437 private static function cleanupStaleAsyncQueue($maxAge)
438 {
439 if (!is_dir(self::$asyncQueueDir)) {
440 return 0;
441 }
442
443 $removed = 0;
444 $now = time();
445
446 try {
447 $queueFiles = @glob(self::$asyncQueueDir . '/*.queue');
448
449 if (!is_array($queueFiles)) {
450 return 0;
451 }
452
453 foreach ($queueFiles as $queueFile) {
454 try {
455 // Remove files older than maxAge
456 if ($now - @filemtime($queueFile) > $maxAge) {
457 @unlink($queueFile);
458 $removed++;
459 }
460 } catch (Exception $e) {
461 // Skip
462 }
463 }
464 } catch (Exception $e) {
465 // Ignore cleanup errors
466 }
467
468 return $removed;
469 }
470
477 public static function reset()
478 {
479 self::$logBuffer = [];
480 self::$shutdownRegistered = false;
481 self::$logStats = [
482 'buffered' => 0,
483 'flushed' => 0,
484 'writes' => 0,
485 'async' => 0,
486 ];
487 }
488
495 public static function setAsyncEnabled($enabled)
496 {
497 self::$asyncEnabled = (bool)$enabled;
498 }
499
505 public static function isAsyncEnabled()
506 {
507 return self::$asyncEnabled;
508 }
509
515 public static function getAsyncQueueDir()
516 {
517 return self::$asyncQueueDir;
518 }
519
525 public static function getAsyncQueueSize()
526 {
527 if (!is_dir(self::$asyncQueueDir)) {
528 return 0;
529 }
530
531 $files = @glob(self::$asyncQueueDir . '/*.queue');
532 return is_array($files) ? count($files) : 0;
533 }
534
540 public static function getStats()
541 {
542 return self::$logStats;
543 }
544
551 public static function setBufferSize($size)
552 {
553 if ($size > 0 && $size <= 1000) {
554 self::$logBufferSize = $size;
555 }
556 }
557
563 public static function getBufferSize()
564 {
565 return self::$logBufferSize;
566 }
567
573 public static function separator()
574 {
575 global $bearsamppRoot;
576
577 $logs = [
585 ];
586
587 $separator = '========================================================================================' . PHP_EOL;
588 foreach ($logs as $log) {
589 if (!file_exists($log)) {
590 continue;
591 }
592 $logContent = @file_get_contents($log);
593 if ($logContent !== false && !str_ends_with($logContent, $separator)) {
594 file_put_contents($log, $separator, FILE_APPEND);
595 }
596 }
597 }
598
605 public static function trace($data, $file = null)
606 {
607 self::write($data, self::TRACE, $file);
608 }
609
616 public static function debug($data, $file = null)
617 {
618 self::write($data, self::DEBUG, $file);
619 }
620
627 public static function info($data, $file = null)
628 {
629 self::write($data, self::INFO, $file);
630 }
631
638 public static function warning($data, $file = null)
639 {
640 self::write($data, self::WARNING, $file);
641 }
642
650 public static function error($data, $file = null)
651 {
652 self::write($data, self::ERROR, $file);
653 }
654
660 public static function initClass($classInstance)
661 {
662 self::trace('Init ' . get_class($classInstance));
663 }
664
670 public static function reloadClass($classInstance)
671 {
672 self::trace('Reload ' . get_class($classInstance));
673 }
674}
675
global $bearsamppRoot
global $bearsamppCore
const VERBOSE_REPORT
const VERBOSE_SIMPLE
const VERBOSE_TRACE
const VERBOSE_DEBUG
static queueAsyncWrite($file, $content)
static flush()
static setBufferSize($size)
static $logStats
Definition class.log.php:47
static info($data, $file=null)
static getBufferSize()
static reset()
static getAsyncQueueSize()
static $asyncQueueDir
Definition class.log.php:55
static debug($data, $file=null)
static initializeAsyncQueue()
static $asyncEnabled
Definition class.log.php:58
static setAsyncEnabled($enabled)
static getStats()
static $logBuffer
Definition class.log.php:38
const DEBUG
Definition class.log.php:34
const ERROR
Definition class.log.php:31
static warning($data, $file=null)
static $logBufferSize
Definition class.log.php:41
static reloadClass($classInstance)
static processAsyncQueue()
static write($data, $type, $file=null)
static $maxAsyncQueueBatch
Definition class.log.php:61
static init()
Definition class.log.php:72
static $shutdownRegistered
Definition class.log.php:44
static trace($data, $file=null)
static checkVerbosityAndAdjustAsync()
Definition class.log.php:94
static error($data, $file=null)
static getAsyncQueueDir()
static isAsyncEnabled()
const WARNING
Definition class.log.php:32
static flushAsyncQueue()
const TRACE
Definition class.log.php:35
static separator()
const INFO
Definition class.log.php:33
static initClass($classInstance)
static cleanupStaleAsyncQueue($maxAge)
static getBatchLogFilePath($aetrayPath=false)
static getLogFilePath($aetrayPath=false)
static getStartupLogFilePath($aetrayPath=false)
static getServicesLogFilePath($aetrayPath=false)
static getRegistryLogFilePath($aetrayPath=false)
static getTmpPath($aetrayPath=false)
static getHomepageLogFilePath($aetrayPath=false)
static getErrorLogFilePath($aetrayPath=false)
static getWinbinderLogFilePath($aetrayPath=false)
global $bearsamppConfig
Definition homepage.php:41
const APP_TITLE
Definition root.php:13