Files
whatsapp/classes/LoggerFactory.php
T
2026-01-27 23:56:49 -05:00

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();