<?php
namespace Fury\Kernel;
/**
* Class for free logging
*/
class Logging extends \Fury\Libs\Singleton{
/**
* Storage for session logs
*
* @var arrray
*/
protected $storage;
/**
* Unique ID of SESSION
*
* @var string
*/
protected $session_id;
/**
* Path to folder with logs
*
* @var string
*/
public $logs_folder;
public function __construct(){
if(!FCONF['logs_enable'])
return false;
$this -> storage = [];
$this -> session_id = uniqid();
$this -> logs_folder = FCONF['logs_folder'];
}
/**
* Set new log item (legacy session-based API)
*
* @method set
*
* @param string $place String in format "Classname@methname" or "funcname"
* @param string $title Title of log.
* @param string $message Any text message
*/
public function set($place, $title, $message){
if(!FCONF['logs_enable'])
return false;
if(strpos($place, '@') === false){
$class = '';
$meth = $place;
}else{
list($class, $meth) = explode('@', $place);
}
$this -> storage[] = [
'class' => $class,
'meth' => $meth,
'title' => $title,
'message' => $message,
'timestamp' => microtime(true)
];
return true;
}
// ============================================================
// Structured JSON Lines logging (new)
// ============================================================
const LEVEL_TRACE = 'TRACE';
const LEVEL_DEBUG = 'DEBUG';
const LEVEL_INFO = 'INFO';
const LEVEL_WARN = 'WARN';
const LEVEL_ERROR = 'ERROR';
const LEVEL_FATAL = 'FATAL';
protected static $write_counter = 0;
/**
* Get current request/session correlation id
*/
public function get_request_id(): string {
return $this -> session_id;
}
public function trace(string $source, string $message, array $context = []){
return $this -> log(self::LEVEL_TRACE, $source, $message, $context);
}
public function debug(string $source, string $message, array $context = []){
return $this -> log(self::LEVEL_DEBUG, $source, $message, $context);
}
public function info(string $source, string $message, array $context = []){
return $this -> log(self::LEVEL_INFO, $source, $message, $context);
}
public function warn(string $source, string $message, array $context = []){
return $this -> log(self::LEVEL_WARN, $source, $message, $context);
}
public function error(string $source, string $message, array $context = []){
return $this -> log(self::LEVEL_ERROR, $source, $message, $context);
}
public function fatal(string $source, string $message, array $context = []){
return $this -> log(self::LEVEL_FATAL, $source, $message, $context);
}
/**
* Write a structured log entry immediately to JSON Lines file
*/
public function log(string $level, string $source, string $message, array $context = []){
if(!FCONF['logs_enable'])
return false;
if($this -> is_level_below_min($level))
return false;
if(!$this -> is_source_allowed($source))
return false;
$entry = [
'ts' => date('c'),
'level' => $level,
'source' => $source,
'request_id' => $this -> session_id,
'message' => $message,
'context' => $this -> redact($context),
];
$line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
$path = $this -> get_jsonl_path();
$this -> rotate_if_needed($path);
$this -> cleanup_if_needed();
$fp = fopen($path, 'a');
if($fp === false)
return false;
if(flock($fp, LOCK_EX)){
fwrite($fp, $line);
fflush($fp);
flock($fp, LOCK_UN);
}
fclose($fp);
chmod($path, 0644);
return true;
}
// ============================================================
// Config helpers
// ============================================================
private function conf(string $key, $default = null){
return FCONF[$key] ?? $default;
}
private function is_level_below_min(string $level): bool {
$map = [
self::LEVEL_TRACE => 0,
self::LEVEL_DEBUG => 1,
self::LEVEL_INFO => 2,
self::LEVEL_WARN => 3,
self::LEVEL_ERROR => 4,
self::LEVEL_FATAL => 5,
];
$min = strtoupper($this -> conf('logs_min_level', self::LEVEL_INFO));
return ($map[$level] ?? 2) < ($map[$min] ?? 2);
}
private function is_source_allowed(string $source): bool {
$allowed = $this -> conf('logs_sources', []);
if(empty($allowed))
return true;
return in_array($source, $allowed, true);
}
// ============================================================
// File path, rotation, cleanup
// ============================================================
private function get_jsonl_path(): string {
$folder = $this -> logs_folder;
if(!is_dir($folder)){
mkdir($folder, 0750, true);
}
return $folder . '/' . date('Y-m-d') . '.jsonl';
}
private function rotate_if_needed(string $path): void {
if(!file_exists($path))
return;
$max_mb = (int) $this -> conf('logs_max_file_size_mb', 50);
$max_bytes = $max_mb * 1024 * 1024;
if(filesize($path) < $max_bytes)
return;
$rotated = $path . '.' . date('Y-m-d_H-i-s');
@rename($path, $rotated);
}
private function cleanup_if_needed(): void {
self::$write_counter++;
if(self::$write_counter % 100 !== 0)
return;
$max_days = (int) $this -> conf('logs_max_days', 30);
$cutoff = time() - ($max_days * 86400);
$folder = $this -> logs_folder;
foreach(glob($folder . '/*.jsonl*') as $file){
if(filemtime($file) < $cutoff){
@unlink($file);
}
}
}
// ============================================================
// Redaction
// ============================================================
private function redact(array $context): array {
$pattern = '/password|token|authorization|secret|key/i';
return $this -> redact_recursive($context, $pattern);
}
private function redact_recursive($data, string $pattern) {
if(is_array($data)){
$result = [];
foreach($data as $k => $v){
if(preg_match($pattern, (string)$k)){
$result[$k] = '***';
}else{
$result[$k] = $this -> redact_recursive($v, $pattern);
}
}
return $result;
}
return $data;
}
/**
* Dumping session logs to json file (legacy format)
*
* @method dump
*
* @return boolean Result of writing to log file
*/
public function dump(){
$log_filename = date('d.m.Y') . '.log.json';
$path_to_log_file = $this -> logs_folder . '/' . $log_filename;
$session = [
'session_id' => $this -> session_id,
'timestamp' => microtime(true),
'logs' => $this -> storage
];
if(!is_dir($this -> logs_folder)){
mkdir($this -> logs_folder, 0750, true);
}
$fp = fopen($path_to_log_file, 'c+');
if($fp === false) {
return false;
}
if(!flock($fp, LOCK_EX)) {
fclose($fp);
return false;
}
$raw = stream_get_contents($fp);
$logs = $raw ? json_decode($raw, true) : [];
if(!is_array($logs)) {
$logs = [];
}
$logs[] = $session;
ftruncate($fp, 0);
rewind($fp);
$result = fwrite($fp, json_encode($logs, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT));
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
chmod($path_to_log_file, 0640);
return $result !== false;
}
}