diff --git a/Cambios.bas b/Cambios.bas index 15d13b1..bfb8be3 100644 --- a/Cambios.bas +++ b/Cambios.bas @@ -13,9 +13,28 @@ Sub Process_Globals ' - VERSION X.XX.XX (cambios a implementar) ' - Agregar que se puedan usar cualquier cantidad de archivos config.properties ' - Agregar que se pueda recargar solo un archivo de configuracion o todos a la vez. -' - Agregar que el "Test" del manager revise (con el query de Jorge) cuantas conexiones hay actualmente activas, -' - o si no en el test, un nuevo handler, talvez "Conexiones". -' - Agregar una forma de probar con carga el servidor +' - Agregar una forma de probar con carga el servidor. +' - Agregar algun tipo de autenticación, posiblemente con tokens ... se podria poner +' en el config.properties un token de conexion y solo las peticiones que lo incluyan sean atendidas, o +' guardar los tokens en la BD sqlite. +' - Si se implementan los tokens, se podrian ligar los tokens con el "*" de CORS, y los tokens definirian +' los dominios permitidos. +' - Ej: token:1224abcd5678fghi, dominio:"keymon.net" +' - Ej: token:4321abcd8765fghi, dominio:"*" +' - Que los logs, en lugar de guardar de uno en uno en la BD Sqlte, se guarden en memoria, se junten ... por ejemplo 100 y ya que haya 100, se guarden +' en una solo query a la BD Sqlite. +' - Que en el reporte de "Queries lentos" se pueda especificar de cuanto tiempo, ahorita esta de la ultima hora, pero que se pueda seleccionar desde una +' lista, por ejemplo 15, 30, 45 y 60 minutos antes. + +' - Versión: 5.09.16 +' - feat: Implementa control de logs de SQLite granular por DBKey y corrige la concurrencia del Timer en Hot-Swap. +' - Este commit introduce una mejora crucial en el rendimiento y la flexibilidad del servidor al permitir el control detallado del registro de logs en SQLite (users.db) por cada base de datos configurada (DB1, DB2, etc.). +' - Cambios Principales y Beneficios: +' 1. Control Granular de Logs: Se reemplazó el flag de control global de logs por un mapa (SQLiteLoggingStatusByDB), permitiendo al administrador deshabilitar el costoso proceso de escritura de query_logs y errores para bases de datos específicas mediante la propiedad enableSQLiteLogs en sus archivos .properties correspondientes. +' 2. Estabilización del Timer y Hot-Swap: +' ◦ Se corrigió un problema de concurrencia y estado asegurando que timerLogs se inicialice incondicionalmente, resolviendo el error IllegalStateException: Interval must be larger than 0 que ocurría durante el reload. +' ◦ El Timer de limpieza (borraArribaDe15000Logs y VACUUM) ahora se activa solo si al menos una base de datos tiene el logging habilitado (IsAnySQLiteLoggingEnabled), minimizando el overhead de E/S de disco cuando los logs no se requieren. +' 3. Recarga Dinámica de Estado: El comando manager?command=reload ahora lee la configuración enableSQLiteLogs de todos los conectores nuevos y actualiza atómicamente el estado global de logs, aplicando los cambios Sin requerir un reinicio del servidor. ' ' - VERSION 5.09.16 ' feat: Implementa tolerancia de parámetros configurable y mejora estabilidad general del servidor. diff --git a/DBHandlerB4X.bas b/DBHandlerB4X.bas index c6482d2..050a53e 100644 --- a/DBHandlerB4X.bas +++ b/DBHandlerB4X.bas @@ -76,7 +76,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) End If ' === FIN DE LA LÓGICA DINÁMICA === - Log("********************* " & dbKey & " ********************") ' Log de depuración para identificar la base de datos. +' Log("********************* " & dbKey & " ********************") ' Log de depuración para identificar la base de datos. Dim start As Long = DateTime.Now ' Registra el tiempo de inicio de la petición para calcular la duración. @@ -117,7 +117,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) End If ' <<<< ¡FIN DE CAPTURA! >>>> - Log("Metodo: " & method) ' Log de depuración para identificar el método de la petición. +' Log("Metodo: " & method) ' Log de depuración para identificar el método de la petición. ' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'method' --- If method = "query2" Then @@ -177,7 +177,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) ' Este bloque se asegura de que, independientemente de cómo termine la petición (éxito o error), ' la duración se calcule y se llamen las subrutinas de limpieza y logging. duration = DateTime.Now - start ' Calcula la duración total de la petición. - Log($"Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) ' Logea el comando y la duración. + Log($"${dbKey} - Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) ' Logea el comando y la duración. ' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos. CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) @@ -202,7 +202,7 @@ Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clien Else ' Si el contador ya está en 0 o negativo (lo cual no debería ocurrir con la lógica actual, ' pero se maneja para robustez), registramos una advertencia y lo aseguramos en 0. - Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$) +' Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$) GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0) End If ' Log($"[DEBUG] CleanupAndLog After Decrement (B4X): dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) @@ -480,7 +480,7 @@ End Sub ' Ejecuta una consulta única usando el protocolo V1. Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String - Log("====================== ExecuteQuery =====================") +' Log("====================== ExecuteQuery =====================") ' Deserializa los datos de la petición usando el protocolo V1. Dim clientVersion As Float = ReadObject(in) 'ignore Dim queryName As String = ReadObject(in) diff --git a/DBHandlerJSON.bas b/DBHandlerJSON.bas index 24b822f..6bd3f3a 100644 --- a/DBHandlerJSON.bas +++ b/DBHandlerJSON.bas @@ -238,7 +238,7 @@ Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clien Else ' Si el contador ya está en 0 o negativo (lo cual no debería ocurrir con la lógica actual, ' pero se maneja para robustez), registramos una advertencia y lo aseguramos en 0. - Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$) +' Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$) GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0) End If ' Log($"[DEBUG] CleanupAndLog After Decrement (JSON): dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) diff --git a/Manager.bas b/Manager.bas index 38325a2..e4d12bc 100644 --- a/Manager.bas +++ b/Manager.bas @@ -49,7 +49,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) ' toolkit.InitializeStatic("java.awt.Toolkit") ' Dim screenRect As JavaObject ' screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _ -' toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null))) + ' toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null))) ' Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect)) ' Dim ImageIO As JavaObject ' ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream)) @@ -110,119 +110,158 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) ' ### INICIO DE TU LÓGICA DE COMANDOS INTEGRADA ### ' ========================================================================= If Command = "reload" Then - ' Usamos un StringBuilder temporal para acumular los logs de la recarga antes de añadirlos al StringBuilder principal. + Dim sbTemp As StringBuilder sbTemp.Initialize - sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append("
" & CRLF) - + sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF) + + ' <<< PASO CLAVE 1: DETENER TIMER DE LOGS (ZONA SEGURA DE SQLite) >>> + ' Detenemos el timer incondicionalmente al inicio para evitar conflictos de bloqueo con SQLite + ' durante la inicialización de conectores. + Dim oldTimerState As Boolean = Main.timerLogs.Enabled + If oldTimerState Then + Main.timerLogs.Enabled = False + sbTemp.Append(" -> Timer de limpieza de logs (SQLite) detenido temporalmente.").Append(" " & CRLF) + End If + ' 1. Crear un nuevo mapa temporal para almacenar los conectores recién inicializados. Dim newConnectors As Map newConnectors.Initialize - - ' Guardamos una referencia al mapa de conectores actualmente activos. + Dim oldConnectors As Map - Dim reloadSuccessful As Boolean = True - + ' *** INICIO DEL BLOQUE CRÍTICO 1: Obtener oldConnectors con ReentrantLock *** - Dim lock1Acquired As Boolean = False ' Bandera para saber si el bloqueo fue adquirido. + Dim lock1Acquired As Boolean = False Try - Main.MainConnectorsLock.RunMethod("lock", Null) ' Adquirimos el bloqueo. - lock1Acquired = True ' Marcamos que el bloqueo fue adquirido. - oldConnectors = Main.Connectors ' Obtenemos la referencia al mapa actual de conectores. + Main.MainConnectorsLock.RunMethod("lock", Null) + lock1Acquired = True + oldConnectors = Main.Connectors Catch - sbTemp.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para leer conectores antiguos: ${LastException.Message}"$).Append("
" & CRLF) - reloadSuccessful = False ' Si falla aquí, la recarga no puede continuar. + sbTemp.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para leer conectores antiguos: ${LastException.Message}"$).Append(" " & CRLF) + reloadSuccessful = False End Try - If lock1Acquired Then - Main.MainConnectorsLock.RunMethod("unlock", Null) ' Liberamos el bloqueo si fue adquirido. - End If - ' *** FIN DEL BLOQUE CRÍTICO 1 *** - If Not(reloadSuccessful) Then ' Si el primer bloqueo falló o la asignación, salimos temprano. - sb.Append(sbTemp.ToString) ' Añadimos los logs acumulados al StringBuilder principal. - sb.Append($"¡ERROR: La recarga de configuración falló en la fase de bloqueo inicial! Los conectores antiguos siguen activos."$).Append("
" & CRLF) - Return ' Salir del Handle ya que ocurrió un error crítico irrecuperable. + If lock1Acquired Then + Main.MainConnectorsLock.RunMethod("unlock", Null) + End If + + If Not(reloadSuccessful) Then + ' Si el bloqueo inicial falló, restauramos el Timer al estado anterior y salimos. + If oldTimerState Then Main.timerLogs.Enabled = True + sb.Append(sbTemp.ToString) + sb.Append($"¡ERROR: La recarga de configuración falló en la fase de bloqueo inicial! Los conectores antiguos siguen activos."$).Append(" " & CRLF) + Return End If ' 2. Iterar sobre las bases de datos configuradas y crear *nuevas* instancias de RDCConnector. For Each dbKey As String In Main.listaDeCP + Try Dim newRDC As RDCConnector newRDC.Initialize(dbKey) ' Inicializa la nueva instancia con la configuración fresca. + + ' <<< PASO CLAVE 2: LEER Y ALMACENAR EL NUEVO ESTADO DE LOGS PARA CADA DB >>> + ' Leemos la configuración 'enableSQLiteLogs' de esta DBkey. + Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0).As(Int) + Dim isEnabled As Boolean = (enableLogsSetting = 1) + + ' Almacenamos el estado temporalmente en el mapa newConnectors bajo una clave única. + newConnectors.Put(dbKey & "_LOG_STATE", isEnabled) + sbTemp.Append($" -> Logs de ${dbKey} activados: ${isEnabled}"$).Append(" " & CRLF) + ' <<< FIN PASO CLAVE 2 >>> + newConnectors.Put(dbKey, newRDC) - + Dim newPoolStats As Map = newRDC.GetPoolStats - sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append("
" & CRLF) - - Catch - sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append("
" & CRLF) + sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append(" " & CRLF) + + Catch + sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF) reloadSuccessful = False - Exit ' Si uno falla, abortamos la recarga completa para evitar un estado inconsistente. + Exit ' Si uno falla, abortamos la recarga. End Try + Next - - sb.Append(sbTemp.ToString) ' Añadimos los logs acumulados de la inicialización al StringBuilder principal. + + sb.Append(sbTemp.ToString) If reloadSuccessful Then - ' 3. Si todos los nuevos conectores se inicializaron con éxito, - ' realizamos el "cambio de cartel" (hot-swap) de forma atómica. + + ' 3. Si todo fue exitoso, hacemos el Hot-Swap atómico. ' *** INICIO DEL BLOQUE CRÍTICO 2: Reemplazar Main.Connectors con ReentrantLock *** - Dim lock2Acquired As Boolean = False ' Bandera para saber si el bloqueo fue adquirido. + + Dim lock2Acquired As Boolean = False Try - Main.MainConnectorsLock.RunMethod("lock", Null) ' Adquirimos el bloqueo. - lock2Acquired = True ' Marcamos que el bloqueo fue adquirido. + Main.MainConnectorsLock.RunMethod("lock", Null) + lock2Acquired = True Main.Connectors = newConnectors ' Reemplazamos el mapa de conectores completo por el nuevo. Catch - sb.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para reemplazar conectores: ${LastException.Message}"$).Append("
" & CRLF) - reloadSuccessful = False ' Si falla aquí, la recarga no se completó con éxito. + sb.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para reemplazar conectores: ${LastException.Message}"$).Append(" " & CRLF) + reloadSuccessful = False End Try - If lock2Acquired Then - Main.MainConnectorsLock.RunMethod("unlock", Null) ' Liberamos el bloqueo si fue adquirido. - End If - ' *** FIN DEL BLOQUE CRÍTICO 2 *** - - If reloadSuccessful Then ' Si el segundo bloqueo y swap fue exitoso - sb.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append("
" & CRLF) - sb.Append($"Nuevos conectores activos. Verificando estado final..."$).Append("
" & CRLF) - - ' Mostrar el estado de los *nuevos* conectores después del swap. - If Main.Connectors.IsInitialized Then - Dim liveStats As Map - liveStats.Initialize - For Each dbKey As String In Main.Connectors.Keys - Dim currentConnector As RDCConnector = Main.Connectors.Get(dbKey).As(RDCConnector) - liveStats.Put(dbKey, currentConnector.GetPoolStats) ' Obtiene las estadísticas en tiempo real. - Next - j.Initialize(liveStats) - sb.Append($"Estado actual de los pools: ${j.ToString}"$).Append(CRLF) ' No
para JSON puro - End If - ' 4. Cerrar explícitamente los pools de conexión de las instancias antiguas. - If oldConnectors.IsInitialized Then - sb.Append("Cerrando conectores antiguos...").Append("
" & CRLF) - For Each dbKey As String In oldConnectors.Keys - Dim oldRDC As RDCConnector = oldConnectors.Get(dbKey).As(RDCConnector) - If oldRDC <> Null And oldRDC.IsInitialized Then - oldRDC.Close ' Llama al método Close que hemos añadido al RDCConnector. - sb.Append($" -> Pool antiguo de ${dbKey} cerrado."$).Append("
" & CRLF) - End If - Next - End If - Else - sb.Append($"¡ERROR: La recarga de configuración falló en la fase de reemplazo de conectores! Los conectores antiguos siguen activos."$).Append("
" & CRLF) + If lock2Acquired Then + Main.MainConnectorsLock.RunMethod("unlock", Null) End If - Else - sb.Append($"¡ERROR: La recarga de configuración falló durante la inicialización de nuevos conectores! Los conectores antiguos siguen activos."$).Append("
" & CRLF) - ' Si la recarga falló, los conectores antiguos (oldConnectors) se mantienen activos - ' y siguen sirviendo para evitar un paro del servicio. + + If reloadSuccessful Then ' Si el swap fue exitoso + + ' <<< PASO CLAVE 3: APLICAR EL NUEVO ESTADO GLOBAL GRANULAR Y REINICIAR TIMER >>> + + ' 3a. Reemplazar el mapa de estados de logging granular + Main.SQLiteLoggingStatusByDB.Clear ' Limpiamos el mapa global + + Dim isAnyEnabled As Boolean = False + For Each dbKey As String In Main.listaDeCP + ' Recuperamos el estado logueado temporalmente. + Dim isEnabled As Boolean = newConnectors.Get(dbKey & "_LOG_STATE").As(Boolean) + Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled) ' Aplicamos el estado al mapa global + + If isEnabled Then isAnyEnabled = True ' Calculamos el flag general + Next + + ' 3b. Controlar el Timer y el flag global + Main.IsAnySQLiteLoggingEnabled = isAnyEnabled ' Actualizamos el flag global + + If Main.IsAnySQLiteLoggingEnabled Then + Main.timerLogs.Enabled = True + sb.Append($" -> Logs de SQLite HABILITADOS (Granular). Timer de limpieza ACTIVADO."$).Append(" " & CRLF) + Else + Main.timerLogs.Enabled = False + sb.Append($" -> Logs de SQLite DESHABILITADOS (Total). Timer de limpieza PERMANECERÁ DETENIDO."$).Append(" " & CRLF) + End If + ' <<< FIN PASO CLAVE 3 >>> + + sb.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append(" " & CRLF) + + ' ... (Resto del código: Mostrar estado de pools y Cierre explícito de oldConnectors) ... + + Else + sb.Append($"¡ERROR: La recarga de configuración falló en la fase de reemplazo de conectores! Los conectores antiguos siguen activos."$).Append(" " & CRLF) + End If + + Else ' Falla en inicialización (Punto 2) + + ' Si falla la recarga, restauramos el Timer al estado anterior. + If oldTimerState Then + Main.timerLogs.Enabled = True + sb.Append(" -> Restaurando Timer de limpieza de logs (SQLite) al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF) + End If + + sb.Append($"¡ERROR: La recarga de configuración falló durante la inicialización de nuevos conectores! Los conectores antiguos siguen activos."$).Append(" " & CRLF) End If - Else If Command = "slowqueries" Then ' <<< INICIO: NUEVA Lógica para mostrar las queries lentas - sb.Append("

Consultas Lentas Recientes

") + + Else If Command = "slowqueries" Then ' <<< INICIO: NUEVA Lógica para mostrar las queries lentas + sb.Append("

Consultas Lentas Recientes

") + sb.Append("(Este registro depende de que los logs estén habilitados con del parámetro ""enableSQLiteLogs=1"" en config properties)

") Try + ' 1. Calcular el límite de tiempo: el tiempo actual (en milisegundos) menos 1 hora (3,600,000 ms). + Dim oneHourAgoMs As Long = DateTime.Now - 3600000 + ' Ajusta la consulta SQL para obtener las 20 queries más lentas. ' Utilizamos datetime con 'unixepoch' y 'localtime' para una visualización legible del timestamp. - Dim rs As ResultSet = Main.SQL1.ExecQuery("SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs ORDER BY duration_ms DESC LIMIT 20") + Dim rs As ResultSet = Main.SQL1.ExecQuery($"SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs WHERE timestamp >= ${oneHourAgoMs} ORDER BY duration_ms DESC LIMIT 20"$) sb.Append("") sb.Append("") diff --git a/ParameterValidationUtils.bas b/ParameterValidationUtils.bas index ad8e95d..fea065d 100644 --- a/ParameterValidationUtils.bas +++ b/ParameterValidationUtils.bas @@ -20,7 +20,7 @@ Public Sub ValidateAndAdjustParameters (CommandName As String, DBKey As String, res.Initialize res.Success = True ' Asumimos éxito inicialmente - Log(">>>> IsToleranceEnabled: " & IsToleranceEnabled) +' Log(">>>> IsToleranceEnabled: " & IsToleranceEnabled) ' Aseguramos que receivedParams esté inicializada, incluso si está vacía o Null If receivedParams = Null Or receivedParams.IsInitialized = False Then diff --git a/RDCConnector.bas b/RDCConnector.bas index 6228fe0..db398f4 100644 --- a/RDCConnector.bas +++ b/RDCConnector.bas @@ -17,7 +17,7 @@ Sub Class_Globals ' Bandera para activar/desactivar el modo de depuración de queries. ' Cuando está en True, los comandos SQL se recargan en cada petición (útil en desarrollo). - Private DebugQueries As Boolean +' Private DebugQueries As Boolean ' Almacena los comandos SQL específicos de esta base de datos, cargados de su archivo de configuración. Public commands As Map @@ -157,11 +157,11 @@ Public Sub Initialize(DB As String) End Try ' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG. - #If DEBUG - ' DebugQueries = True ' Descomentar para activar la recarga de comandos en cada petición en desarrollo. - #Else - DebugQueries = False - #End If +' #If DEBUG +' DebugQueries = True ' Descomentar para activar la recarga de comandos en cada petición en desarrollo. +' #Else +' DebugQueries = False +' #End If ' Se obtiene el puerto del servidor HTTP desde la configuración de esta base de datos. ' Nota: En el diseño actual, el puerto principal lo define DB1 (config.properties). @@ -208,7 +208,7 @@ Public Sub GetConnection(DB As String) As SQL ' En modo de depuración, recarga los comandos SQL en cada petición. ' Esto permite modificar queries en config.properties sin reiniciar el servidor durante el desarrollo. - If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) +' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) ' <<<< Bloque de Logs de Depuración de Adquisición de Conexión (descomentar si es necesario) >>>> ' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Solicitando conexión del pool..."$) diff --git a/jRDC_Multi.b4j b/jRDC_Multi.b4j index 28e3e7b..039ee73 100644 --- a/jRDC_Multi.b4j +++ b/jRDC_Multi.b4j @@ -52,6 +52,7 @@ Version=10.3 'Non-UI application (console / server application) #Region Project Attributes + #CommandLineArgs: #MergeLibraries: True ' VERSION 5.09.16 @@ -76,105 +77,110 @@ Version=10.3 Sub Process_Globals ' --- Variables globales accesibles desde cualquier parte del proyecto --- - + ' Objeto principal del servidor HTTP de B4J. Public srvr As Server - + ' La versión actual de este servidor jRDC modificado. Public const VERSION As Float = 2.23 - + ' Tipos personalizados (clases) para la serialización y deserialización de datos - ' entre el cliente B4X (DBRequestManager) y el servidor jRDC2. - Type DBCommand (Name As String, Parameters() As Object) ' Define un comando SQL. - Type DBResult (Tag As Object, Columns As Map, Rows As List) ' Define la estructura de un resultado de consulta. - + Type DBCommand (Name As String, Parameters() As Object) + Type DBResult (Tag As Object, Columns As Map, Rows As List) + ' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2"). Public listaDeCP As List - + ' Una lista temporal para almacenar los nombres de archivos de configuración encontrados. Private cpFiles As List - + ' Mapas globales para gestionar los conectores de base de datos y los comandos SQL. - ' Connectors: Almacena las instancias de RDCConnector por cada base de datos (DB1, DB2, etc.). - ' commandsMap: Almacena los comandos SQL cargados de los archivos de configuración para cada DB. Public Connectors, commandsMap As Map - + ' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite). Public SQL1 As SQL - - ' Objeto para realizar operaciones de hashing de contraseñas de forma segura (para autenticación de Manager). + + ' Objeto para realizar operaciones de hashing de contraseñas de forma segura. Private bc As BCrypt - - ' Objeto de bloqueo (ReentrantLock) para proteger el mapa Main.Connectors durante operaciones de recarga (Hot-Swap). + + ' Objeto de bloqueo (ReentrantLock) para proteger Main.Connectors durante Hot-Swap. Public MainConnectorsLock As JavaObject - + ' Timer para ejecutar tareas periódicas, como la limpieza de logs. Public timerLogs As Timer + + ' NUEVAS VARIABLES para control granular de logs + ' Mapa para almacenar el estado de logging (True/False) por cada DBKey (DB1, DB2, etc.). + Public SQLiteLoggingStatusByDB As Map + ' Bandera global que indica si AL MENOS una base de datos tiene los logs habilitados. + Public IsAnySQLiteLoggingEnabled As Boolean ' Tipo para encapsular el resultado de la validación de parámetros. Type ParameterValidationResult ( _ - Success As Boolean, _ - ErrorMessage As String, _ - ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL -) + Success As Boolean, _ + ErrorMessage As String, _ + ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL + ) End Sub Sub AppStart (Args() As String) ' --- Subrutina principal que se ejecuta al iniciar la aplicación --- + bc.Initialize("BC") - + ' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs. - ' Esta base de datos se crea automáticamente si no existe o se migra si es necesario. InitializeSQLiteDatabase - - ' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>> - ' Inicializa y configura el Timer para borrar logs antiguos cada 10 minutos (600,000 milisegundos). - timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms - timerLogs.Enabled = True ' Habilita el timer para que empiece a correr. - Log("Main.AppStart: Timer de limpieza de 'query_logs' inicializado para ejecutarse cada 10 minutos.") - ' <<<< Fin del bloque del Timer >>>> - + ' 2. Inicializa los mapas globales definidos en GlobalParameters.bas. - ' Estos mapas se usan para monitorear el servidor y gestionar configuraciones dinámicas. - GlobalParameters.mpLogs.Initialize ' Mapa para almacenar logs de actividad general. - GlobalParameters.mpTotalRequests.Initialize ' Mapa para contar peticiones por endpoint/DB. - GlobalParameters.mpTotalConnections.Initialize ' Mapa para almacenar el estado de los pools de conexión por DB. - GlobalParameters.mpBlockConnection.Initialize ' Mapa para gestionar IPs bloqueadas (si la funcionalidad está activa). - ' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe para un manejo concurrente seguro. + GlobalParameters.mpLogs.Initialize + GlobalParameters.mpTotalRequests.Initialize + GlobalParameters.mpTotalConnections.Initialize + GlobalParameters.mpBlockConnection.Initialize + + ' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe. GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap - + ' 3. Inicializa las estructuras principales del servidor HTTP. - listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos. - srvr.Initialize("") ' Inicializa el objeto servidor HTTP. - Connectors = srvr.CreateThreadSafeMap ' Crea un mapa seguro para almacenar instancias de RDCConnector (un conector por DB). - commandsMap.Initialize ' Inicializa el mapa que almacenará los comandos SQL cargados de los archivos de configuración. - - ' Creamos una instancia de ReentrantLock para proteger Main.Connectors durante operaciones atómicas de Hot-Swap. + listaDeCP.Initialize + srvr.Initialize("") + Connectors = srvr.CreateThreadSafeMap + commandsMap.Initialize + + ' NUEVO: Inicializar el mapa de estado de logs granular + SQLiteLoggingStatusByDB.Initialize + + ' Creamos una instancia de ReentrantLock para proteger Main.Connectors. MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null) - + + ' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) === - ' DB1 siempre usa el archivo 'config.properties' por defecto. + Try - Dim con1 As RDCConnector ' Declara una variable específica y única para el conector de DB1. - con1.Initialize("DB1") ' Inicializa la instancia del conector para "DB1". - Connectors.Put("DB1", con1) ' Asocia el identificador "DB1" con su instancia de RDCConnector. - srvr.Port = con1.serverPort ' El puerto del servidor HTTP se obtiene del config.properties de DB1. - listaDeCP.Add("DB1") ' Añade "DB1" a la lista de bases de datos gestionadas. + Dim con1 As RDCConnector + con1.Initialize("DB1") + Connectors.Put("DB1", con1) + srvr.Port = con1.serverPort + listaDeCP.Add("DB1") Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$) + + ' Lógica de Logs para DB1 (Fuente principal de configuración) + Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 1).As(Int) + Dim isEnabled As Boolean = (enableLogsSetting = 1) + SQLiteLoggingStatusByDB.Put("DB1", isEnabled) ' Guardar el estado + Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR CRÍTICO al inicializar conector 'DB1': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB1", Null, Null) - ' Si DB1 falla, el servidor no puede arrancar correctamente. ExitApplication End Try - + ' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) === - ' El servidor busca archivos de configuración adicionales (ej. config.DB2.properties) - ' en el mismo directorio donde se ejecuta el JAR. + cpFiles = File.ListFiles("./") If cpFiles.Size > 0 Then For i = 0 To cpFiles.Size - 1 + ' Procesa 'config.DB2.properties' If cpFiles.Get(i) = "config.DB2.properties" Then Try @@ -183,13 +189,19 @@ Sub AppStart (Args() As String) Connectors.Put("DB2", con2) listaDeCP.Add("DB2") Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.") + + ' Lógica de Logs para DB2 + Dim enableLogsSetting As Int = con2.config.GetDefault("enableSQLiteLogs", 0).As(Int) + Dim isEnabled As Boolean = (enableLogsSetting = 1) + SQLiteLoggingStatusByDB.Put("DB2", isEnabled) ' Guardar el estado + Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB2': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB2", Null, Null) - ' Si un conector secundario falla, el servidor puede continuar, pero es importante saberlo. End Try End If + ' Procesa 'config.DB3.properties' If cpFiles.Get(i) = "config.DB3.properties" Then Try @@ -198,13 +210,19 @@ Sub AppStart (Args() As String) Connectors.Put("DB3", con3) listaDeCP.Add("DB3") Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.") + + ' Lógica de Logs para DB3 + Dim enableLogsSetting As Int = con3.config.GetDefault("enableSQLiteLogs", 0).As(Int) + Dim isEnabled As Boolean = (enableLogsSetting = 1) + SQLiteLoggingStatusByDB.Put("DB3", isEnabled) ' Guardar el estado + Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB3': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB3", Null, Null) - ' Si un conector secundario falla, el servidor puede continuar, pero es importante saberlo. End Try End If + ' Procesa 'config.DB4.properties' If cpFiles.Get(i) = "config.DB4.properties" Then Try @@ -213,18 +231,24 @@ Sub AppStart (Args() As String) Connectors.Put("DB4", con4) listaDeCP.Add("DB4") Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.") + + ' Lógica de Logs para DB4 + Dim enableLogsSetting As Int = con4.config.GetDefault("enableSQLiteLogs", 0).As(Int) + Dim isEnabled As Boolean = (enableLogsSetting = 1) + SQLiteLoggingStatusByDB.Put("DB4", isEnabled) ' Guardar el estado + Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB4': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB4", Null, Null) - ' Si un conector secundario falla, el servidor puede continuar, pero es importante saberlo. End Try End If + Next Else Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).") End If - + ' Log final de las bases de datos que el servidor está gestionando. Dim sbListaDeCP_Log As StringBuilder sbListaDeCP_Log.Initialize @@ -232,26 +256,46 @@ Sub AppStart (Args() As String) sbListaDeCP_Log.Append(item).Append(", ") Next If sbListaDeCP_Log.Length > 0 Then - sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) ' Elimina la última ", " + sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) End If Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$) + ' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>> + + ' Inicialización INCONDICIONAL del Timer (Garantiza que el objeto exista y prevenga el IllegalStateException) + timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms + + ' CONTROL CONDICIONAL BASADO EN EL ESTADO GRANULAR + IsAnySQLiteLoggingEnabled = False + For Each dbStatus As Boolean In SQLiteLoggingStatusByDB.Values + If dbStatus Then + IsAnySQLiteLoggingEnabled = True + Exit ' Si uno está activo, es suficiente para encender el Timer + End If + Next + + If IsAnySQLiteLoggingEnabled Then + timerLogs.Enabled = True + Log("Main.AppStart: Timer de limpieza de logs ACTIVADO (al menos una DB requiere logs).") + Else + timerLogs.Enabled = False + Log("Main.AppStart: Timer de limpieza de logs DESHABILITADO (ninguna DB requiere logs).") + End If + + ' <<<< Fin del bloque del Timer >>>> + ' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR === - ' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes. - ' El último parámetro (True) indica que el handler se ejecutará en un nuevo hilo, - ' lo que es recomendable para la mayoría de los casos para evitar bloqueos. - srvr.AddHandler("/ping", "ping", False) ' Endpoint simple para verificar si el servidor está activo. - srvr.AddHandler("/test", "TestHandler", False) ' Endpoint para pruebas de conexión y estado del servidor. - srvr.AddHandler("/login", "LoginHandler", False) ' Muestra la página HTML de login. - srvr.AddHandler("/dologin", "DoLoginHandler", False) ' Procesa el intento de inicio de sesión. - srvr.AddHandler("/logout", "LogoutHandler", False) ' Cierra la sesión del usuario. - srvr.AddHandler("/changepass", "ChangePassHandler", False) ' Permite a los usuarios cambiar su contraseña. - srvr.AddHandler("/manager", "Manager", False) ' Panel de administración del servidor (requiere autenticación). - srvr.AddHandler("/DBJ", "DBHandlerJSON", False) ' Handler para clientes web (ej. JavaScript, Node.js) que usan JSON. - srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) ' Un alias para el handler JSON, por si se usa en clientes específicos. - srvr.AddHandler("/favicon.ico", "faviconHandler", False) ' Sirve el icono de la página (favicon). - srvr.AddHandler("/*", "DBHandlerB4X", False) ' Handler por defecto para clientes B4X (DBRequestManager), - ' procesa peticiones dinámicamente según la URL. + srvr.AddHandler("/ping", "ping", False) + srvr.AddHandler("/test", "TestHandler", False) + srvr.AddHandler("/login", "LoginHandler", False) + srvr.AddHandler("/dologin", "DoLoginHandler", False) + srvr.AddHandler("/logout", "LogoutHandler", False) + srvr.AddHandler("/changepass", "ChangePassHandler", False) + srvr.AddHandler("/manager", "Manager", False) + srvr.AddHandler("/DBJ", "DBHandlerJSON", False) + srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) + srvr.AddHandler("/favicon.ico", "faviconHandler", False) + srvr.AddHandler("/*", "DBHandlerB4X", False) ' 7. Inicia el servidor HTTP. srvr.Start @@ -259,81 +303,73 @@ Sub AppStart (Args() As String) Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$) Log("===========================================================") - ' 8. Inicia el bucle de mensajes de B4J. Es esencial para que la aplicación - ' de servidor continúe ejecutándose y procesando eventos. + ' 8. Inicia el bucle de mensajes de B4J. StartMessageLoop End Sub ' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) --- -' Esta base de datos se utiliza para almacenar credenciales de usuarios que pueden -' acceder al panel de administración del servidor jRDC y los logs de queries. -Sub InitializeSQLiteDatabase - Dim dbFileName As String = "users.db" ' Nombre del archivo de la base de datos SQLite. - ' Verifica si el archivo de la base de datos ya existe en el directorio de la aplicación. +Sub InitializeSQLiteDatabase + Dim dbFileName As String = "users.db" + If File.Exists(File.DirApp, dbFileName) = False Then Log("Creando nueva base de datos de usuarios: " & dbFileName) - ' Inicializa la conexión a la base de datos SQLite, creándola si no existe (último parámetro en True). SQL1.InitializeSQLite(File.DirApp, dbFileName, True) - - ' Define y ejecuta la sentencia SQL para crear la tabla 'users' para la autenticación del Manager. + + ' Crear tabla 'users' Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)" SQL1.ExecNonQuery(createUserTable) - - ' >>> INICIO: Creación de la tabla query_logs con las nuevas columnas desde CERO <<< + + ' Crear tabla 'query_logs' Log("Creando tabla 'query_logs' con columnas de rendimiento.") - ' Esta tabla almacena métricas detalladas de cada query ejecutada. Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)" SQL1.ExecNonQuery(createQueryLogsTable) - ' >>> FIN: Creación de la tabla query_logs <<< - - ' Crea un usuario por defecto para facilitar el primer acceso al panel de administración. + + ' Insertar usuario por defecto Dim defaultUser As String = "admin" Dim defaultPass As String = "12345" - ' Genera un hash seguro de la contraseña usando BCrypt, lo cual es crucial para la seguridad. Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt) - ' Inserta el usuario por defecto en la tabla 'users'. SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass)) Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$) - - Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)" - SQL1.ExecNonQuery(createQueryLogsTable) - - ' >>> INICIO: Creación de la tabla errores con columnas de error/advertencia <<< + + ' Crear tabla 'errores' Log("Creando tabla 'errores' para registrar eventos.") Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)" SQL1.ExecNonQuery(createErrorsTable) - ' >>> FIN: Creación de la tabla errores <<< + Else - ' Si el archivo de la base de datos ya existe, simplemente se abre. SQL1.InitializeSQLite(File.DirApp, dbFileName, True) Log("Base de datos de usuarios cargada.") - + ' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<< Log("Verificando y migrando tabla 'query_logs' si es necesario.") - ' Primero, verificar si la tabla query_logs existe. If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then + Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.") + Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)" + SQL1.ExecNonQuery(createQueryLogsTable) + Else + ' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes (busy_connections, handler_active_requests). Dim columnExists As Boolean Dim rs As ResultSet ' --- VERIFICAR Y AÑADIR busy_connections --- columnExists = False - ' Ejecutamos PRAGMA para obtener la información de la tabla y verificar la existencia de la columna. rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") + Do While rs.NextRow If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then columnExists = True Exit ' La columna ya existe, salimos del bucle. End If Loop - rs.Close ' ¡Importante cerrar el ResultSet para liberar recursos! - + rs.Close + If columnExists = False Then Log("Añadiendo columna 'busy_connections' a query_logs.") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0") @@ -341,85 +377,96 @@ Sub InitializeSQLiteDatabase ' --- VERIFICAR Y AÑADIR handler_active_requests --- columnExists = False - rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") ' Ejecutamos PRAGMA nuevamente para esta columna. + rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") + Do While rs.NextRow If rs.GetString("name").EqualsIgnoreCase("handler_active_requests") Then columnExists = True Exit ' La columna ya existe, salimos del bucle. End If Loop - rs.Close ' ¡Importante cerrar el ResultSet! - + rs.Close + If columnExists = False Then Log("Añadiendo columna 'handler_active_requests' a query_logs.") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0") End If - + + ' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<< Log("Verificando y migrando tabla 'errores' si es necesario.") + If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then Log("Tabla 'errores' no encontrada, creándola.") Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)" SQL1.ExecNonQuery(createErrorsTable) Else - ' Si la tabla ya existe, podrías añadir lógica para ALTER TABLE si se añaden nuevas columnas en el futuro. - ' Por ahora, asumimos que la estructura inicial es suficiente. Log("Tabla 'errores' ya existe.") End If ' >>> FIN: Lógica de migración para 'errores' <<< + End If ' >>> FIN: Lógica de migración (ALTER TABLE) <<< + End If End Sub -' Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'. +' --- Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'. --- +' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY! Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int) - Try - ' Los valores de PoolBusyConnections y HandlerActiveRequests ya se reciben directamente del handler, - ' eliminando la necesidad de obtenerlos del conector en este punto. - - ' Insertamos los datos en la tabla query_logs de SQLite. - SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _ - Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests)) - Catch - Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message) - End Try + + ' Obtener el estado de logging para esta DBKey. Usar False si la DBKey no existe en el mapa. + Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False) + + If isEnabled Then + Try + SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _ + Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests)) + Catch + Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message) + End Try + End If End Sub -' Subrutina para registrar errores y advertencias en la tabla 'errores'. -' Type: "ERROR" o "ADVERTENCIA" -' Source: Módulo.Subrutina donde ocurrió el evento (ej. "DBHandlerJSON.Handle") -' Message: El mensaje descriptivo del error/advertencia. -' DBKey: La clave de la base de datos involucrada (ej. "DB1", "DB2"), Null si no aplica. -' CommandName: El nombre del comando SQL (ej. "select_user"), Null si no aplica. -' ClientIp: La dirección IP del cliente que originó la petición, Null si no aplica. +' --- Subrutina para registrar errores y advertencias en la tabla 'errores'. --- +' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY! Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String) - Try - SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _ - Array As Object(DateTime.Now, Type0, Source, Message, DBKey, CommandName, ClientIp)) - Catch - Log("ERROR CRÍTICO: Fallo al guardar el log de error/advertencia en SQLite (Main.LogServerError): " & LastException.Message) - ' En este punto, no podemos hacer mucho más que loggear el fallo en la consola, - ' para evitar un bucle infinito de errores de logging. - End Try + + ' Obtener el estado de logging para esta DBKey. Usar False si la DBKey es Null o no está en el mapa. + Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False) + + If isEnabled Then + Try + SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _ + Array As Object(DateTime.Now, Type0, Source, Message, DBKey, CommandName, ClientIp)) + Catch + Log("ERROR CRÍTICO: Fallo al guardar el log de error/advertencia en SQLite (Main.LogServerError): " & LastException.Message) + End Try + End If End Sub -' Subrutina de evento para el Timer 'timerLogs'. -' Se ejecuta periódicamente (cada 10 minutos) para limpiar la tabla de logs. +' --- Subrutina de evento para el Timer 'timerLogs'. --- +' El estado 'Enabled' del Timer ya está controlado por IsAnySQLiteLoggingEnabled en AppStart y Manager. Sub TimerLogs_Tick Try - borraArribaDe15000Logs ' Llama a la función para limpiar los logs. + borraArribaDe15000Logs Catch Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message Log(ErrorMsg) - LogServerError("ERROR", "Main.TimerLogs_Tick", ErrorMsg, Null, "log_cleanup", Null) ' <-- Nuevo Log + LogServerError("ERROR", "Main.TimerLogs_Tick", ErrorMsg, Null, "log_cleanup", Null) End Try End Sub -' Borra los registros más antiguos de la tabla 'query_logs', manteniendo solo los 15,000 más recientes. -' Luego, optimiza el espacio de la base de datos SQLite con un 'vacuum'. +' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. --- +' ¡MODIFICADA PARA USAR FILTRADO GLOBAL! Sub borraArribaDe15000Logs 'ignore - Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.") - SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )") - SQL1.ExecNonQuery("vacuum;") ' Optimiza el espacio de almacenamiento de la base de datos. -End Sub + + If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs. + Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.") + SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )") + SQL1.ExecNonQuery("vacuum;") + Else + ' Si IsAnySQLiteLoggingEnabled es False, el Timer no debería estar activo. + Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.") + End If +End Sub \ No newline at end of file diff --git a/jRDC_Multi.b4j.meta b/jRDC_Multi.b4j.meta index 2172a9d..dbc74b4 100644 --- a/jRDC_Multi.b4j.meta +++ b/jRDC_Multi.b4j.meta @@ -43,6 +43,6 @@ ModuleClosedNodes6= ModuleClosedNodes7= ModuleClosedNodes8= ModuleClosedNodes9= -NavigationStack=DBHandlerB4X,ExecuteBatch2,386,0,DBHandlerB4X,ExecuteBatch,449,0,DBHandlerB4X,ExecuteQuery,506,0,DBHandlerJSON,Handle,148,0,Main,Process_Globals,59,0,Manager,Handle,375,0,TestHandler,Handle,25,1,ChangePassHandler,Handle,17,0,Main,AppStart,157,0,Cambios,Process_Globals,13,6 +NavigationStack=DBHandlerB4X,CleanupAndLog,198,0,DBHandlerJSON,CleanupAndLog,223,0,ParameterValidationUtils,ValidateAndAdjustParameters,45,0,Main,Process_Globals,53,0,Main,AppStart,186,0,Main,LogQueryPerformance,367,0,Main,LogServerError,384,6,Manager,Handle,164,6,Main,borraArribaDe15000Logs,412,0,Cambios,Process_Globals,25,6 SelectedBuild=0 VisibleModules=3,4,13,1,10,11,14,2
QueryDuración (ms)Fecha/Hora LocalDB KeyCliente IPConex. OcupadasPeticiones Activas