up
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
<?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();
|
||||
Reference in New Issue
Block a user