321 lines
9.7 KiB
PHP
321 lines
9.7 KiB
PHP
<?php
|
|
/**
|
|
* Sistema de logging estructurado con Monolog
|
|
* Configuración centralizada para toda la aplicación
|
|
*/
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
|
use Monolog\Logger;
|
|
use Monolog\Handler\StreamHandler;
|
|
use Monolog\Handler\RotatingFileHandler;
|
|
use Monolog\Handler\ErrorLogHandler;
|
|
use Monolog\Formatter\LineFormatter;
|
|
use Monolog\Formatter\JsonFormatter;
|
|
use Monolog\Processor\WebProcessor;
|
|
use Monolog\Processor\IntrospectionProcessor;
|
|
|
|
class LoggerFactory {
|
|
private static $loggers = [];
|
|
private static $defaultLevel = Logger::INFO;
|
|
private static $logPath;
|
|
|
|
/**
|
|
* Inicializar configuración de logging
|
|
*/
|
|
public static function init() {
|
|
self::$logPath = getenv('LOG_PATH') ?: __DIR__ . '/../logs';
|
|
|
|
// Crear directorio de logs si no existe
|
|
if (!is_dir(self::$logPath)) {
|
|
mkdir(self::$logPath, 0755, true);
|
|
}
|
|
|
|
// Configurar nivel según ambiente
|
|
$level = getenv('LOG_LEVEL') ?: 'INFO';
|
|
self::$defaultLevel = constant("Monolog\Logger::" . strtoupper($level));
|
|
}
|
|
|
|
/**
|
|
* Obtener logger para un canal específico
|
|
*
|
|
* @param string $channel Nombre del canal (ej: 'webhook', 'bot', 'api')
|
|
* @param array $options Opciones adicionales
|
|
* @return Logger
|
|
*/
|
|
public static function getLogger(string $channel, array $options = []): Logger {
|
|
// Retornar logger existente si ya está creado
|
|
if (isset(self::$loggers[$channel])) {
|
|
return self::$loggers[$channel];
|
|
}
|
|
|
|
if (!self::$logPath) {
|
|
self::init();
|
|
}
|
|
|
|
$logger = new Logger($channel);
|
|
|
|
// Opciones por defecto
|
|
$defaultOptions = [
|
|
'rotating' => true, // Usar archivos rotativos
|
|
'max_files' => 14, // Mantener últimos 14 días
|
|
'json' => false, // Formato JSON (útil para parseo)
|
|
'include_context' => true, // Incluir información de contexto
|
|
'level' => self::$defaultLevel
|
|
];
|
|
|
|
$options = array_merge($defaultOptions, $options);
|
|
|
|
// Handler para archivo
|
|
if ($options['rotating']) {
|
|
$handler = new RotatingFileHandler(
|
|
self::$logPath . '/' . $channel . '.log',
|
|
$options['max_files'],
|
|
$options['level']
|
|
);
|
|
} else {
|
|
$handler = new StreamHandler(
|
|
self::$logPath . '/' . $channel . '.log',
|
|
$options['level']
|
|
);
|
|
}
|
|
|
|
// Formatter
|
|
if ($options['json']) {
|
|
$formatter = new JsonFormatter();
|
|
} else {
|
|
$formatter = new LineFormatter(
|
|
"[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
|
|
"Y-m-d H:i:s",
|
|
true,
|
|
true
|
|
);
|
|
}
|
|
|
|
$handler->setFormatter($formatter);
|
|
$logger->pushHandler($handler);
|
|
|
|
// Handler adicional para errores críticos (siempre a error_log de PHP)
|
|
$errorLogHandler = new ErrorLogHandler(
|
|
ErrorLogHandler::OPERATING_SYSTEM,
|
|
Logger::ERROR
|
|
);
|
|
$logger->pushHandler($errorLogHandler);
|
|
|
|
// Processors (agregar contexto automático)
|
|
if ($options['include_context']) {
|
|
// Agregar información de la petición web
|
|
$logger->pushProcessor(new WebProcessor());
|
|
|
|
// Agregar información de archivo/línea donde se generó el log
|
|
$logger->pushProcessor(new IntrospectionProcessor(
|
|
Logger::DEBUG,
|
|
['Monolog\\', 'LoggerFactory']
|
|
));
|
|
|
|
// Agregar PID y memoria
|
|
$logger->pushProcessor(function ($record) {
|
|
$record['extra']['pid'] = getmypid();
|
|
$record['extra']['memory_mb'] = round(memory_get_usage(true) / 1024 / 1024, 2);
|
|
return $record;
|
|
});
|
|
}
|
|
|
|
// Guardar logger en caché
|
|
self::$loggers[$channel] = $logger;
|
|
|
|
return $logger;
|
|
}
|
|
|
|
/**
|
|
* Loggers predefinidos para componentes principales
|
|
*/
|
|
public static function webhook(): Logger {
|
|
return self::getLogger('webhook', [
|
|
'rotating' => true,
|
|
'max_files' => 7,
|
|
'level' => Logger::INFO
|
|
]);
|
|
}
|
|
|
|
public static function worker(): Logger {
|
|
return self::getLogger('worker', [
|
|
'rotating' => true,
|
|
'max_files' => 7,
|
|
'level' => Logger::DEBUG
|
|
]);
|
|
}
|
|
|
|
public static function bot(): Logger {
|
|
return self::getLogger('bot', [
|
|
'rotating' => true,
|
|
'max_files' => 14,
|
|
'level' => Logger::INFO
|
|
]);
|
|
}
|
|
|
|
public static function api(): Logger {
|
|
return self::getLogger('api', [
|
|
'rotating' => true,
|
|
'max_files' => 7,
|
|
'level' => Logger::INFO
|
|
]);
|
|
}
|
|
|
|
public static function whatsapp(): Logger {
|
|
return self::getLogger('whatsapp', [
|
|
'rotating' => true,
|
|
'max_files' => 7,
|
|
'level' => Logger::INFO
|
|
]);
|
|
}
|
|
|
|
public static function queue(): Logger {
|
|
return self::getLogger('queue', [
|
|
'rotating' => true,
|
|
'max_files' => 7,
|
|
'level' => Logger::DEBUG
|
|
]);
|
|
}
|
|
|
|
public static function security(): Logger {
|
|
return self::getLogger('security', [
|
|
'rotating' => true,
|
|
'max_files' => 30, // Mantener logs de seguridad por más tiempo
|
|
'level' => Logger::WARNING
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Logger general de aplicación
|
|
*/
|
|
public static function app(): Logger {
|
|
return self::getLogger('app', [
|
|
'rotating' => true,
|
|
'max_files' => 14,
|
|
'level' => Logger::INFO
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Limpiar logs antiguos (ejecutar con cron)
|
|
*/
|
|
public static function cleanup(int $daysToKeep = 30): int {
|
|
$cleaned = 0;
|
|
$cutoff = time() - ($daysToKeep * 86400);
|
|
|
|
if (!is_dir(self::$logPath)) {
|
|
return 0;
|
|
}
|
|
|
|
$files = glob(self::$logPath . '/*.log*');
|
|
|
|
foreach ($files as $file) {
|
|
if (is_file($file) && filemtime($file) < $cutoff) {
|
|
if (unlink($file)) {
|
|
$cleaned++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $cleaned;
|
|
}
|
|
|
|
/**
|
|
* Obtener estadísticas de logs
|
|
*/
|
|
public static function getStats(): array {
|
|
if (!is_dir(self::$logPath)) {
|
|
return ['error' => 'Log directory not found'];
|
|
}
|
|
|
|
$stats = [
|
|
'total_files' => 0,
|
|
'total_size_mb' => 0,
|
|
'by_channel' => []
|
|
];
|
|
|
|
$files = glob(self::$logPath . '/*.log*');
|
|
|
|
foreach ($files as $file) {
|
|
if (!is_file($file)) continue;
|
|
|
|
$stats['total_files']++;
|
|
$size = filesize($file);
|
|
$stats['total_size_mb'] += $size / 1024 / 1024;
|
|
|
|
// Extraer canal del nombre de archivo
|
|
$basename = basename($file);
|
|
preg_match('/^([^-\.]+)/', $basename, $matches);
|
|
$channel = $matches[1] ?? 'unknown';
|
|
|
|
if (!isset($stats['by_channel'][$channel])) {
|
|
$stats['by_channel'][$channel] = [
|
|
'files' => 0,
|
|
'size_mb' => 0
|
|
];
|
|
}
|
|
|
|
$stats['by_channel'][$channel]['files']++;
|
|
$stats['by_channel'][$channel]['size_mb'] += $size / 1024 / 1024;
|
|
}
|
|
|
|
// Redondear tamaños
|
|
$stats['total_size_mb'] = round($stats['total_size_mb'], 2);
|
|
foreach ($stats['by_channel'] as $channel => $data) {
|
|
$stats['by_channel'][$channel]['size_mb'] = round($data['size_mb'], 2);
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Función helper global para logging rápido
|
|
* Mantiene compatibilidad con writeLog() existente
|
|
*/
|
|
if (!function_exists('writeLog')) {
|
|
function writeLog(string $level, string $message, array $context = [], string $channel = 'app') {
|
|
try {
|
|
$logger = LoggerFactory::getLogger($channel);
|
|
|
|
$level = strtolower($level);
|
|
switch ($level) {
|
|
case 'debug':
|
|
$logger->debug($message, $context);
|
|
break;
|
|
case 'info':
|
|
$logger->info($message, $context);
|
|
break;
|
|
case 'notice':
|
|
$logger->notice($message, $context);
|
|
break;
|
|
case 'warning':
|
|
case 'warn':
|
|
$logger->warning($message, $context);
|
|
break;
|
|
case 'error':
|
|
$logger->error($message, $context);
|
|
break;
|
|
case 'critical':
|
|
$logger->critical($message, $context);
|
|
break;
|
|
case 'alert':
|
|
$logger->alert($message, $context);
|
|
break;
|
|
case 'emergency':
|
|
$logger->emergency($message, $context);
|
|
break;
|
|
default:
|
|
$logger->info($message, $context);
|
|
}
|
|
} catch (Exception $e) {
|
|
// Fallback a error_log nativo de PHP
|
|
error_log("[$level] $message " . json_encode($context));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Inicializar al cargar el archivo
|
|
LoggerFactory::init();
|