nsyslog
Version:
Modular new generation log agent. Reads, transform, aggregate, correlate and send logs from sources to destinations
783 lines (672 loc) • 25 kB
Markdown
# Análisis Completo del Módulo FileInput
## Descripción General
El módulo `FileInput` es una clase que extiende la clase base `Input` y proporciona funcionalidades para la lectura, monitoreo y gestión de archivos en un sistema de entrada de datos. Este módulo está diseñado para manejar la lectura de archivos de forma eficiente, con soporte para marcas de agua (watermarks), monitoreo en tiempo real y gestión de múltiples archivos simultáneamente.
## Arquitectura y Diseño
### Herencia y Estructura
- **Clase base**: `Input`
- **Patrón**: Singleton por instancia con semáforos para control de concurrencia
- **Modo de operación**: Pull (modo de extracción)
### Dependencias Principales
```javascript
const Input = require('..'),
Semaphore = require('../../semaphore'),
jsexpr = require('jsexpr'),
fs = require('fs-extra'),
Path = require('path'),
extend = require('extend'),
minimatch = require('minimatch'),
slash = require('slash'),
logger = require("../../logger"),
Watermark = require("../../watermark"),
filesystem = require("./filesystem");
```
## Funcionalidades Principales
### 1. Gestión de Archivos
- **Apertura y lectura de archivos**: Manejo de descriptores de archivo con control de concurrencia
- **Monitoreo de archivos**: Capacidad de vigilar directorios para nuevos archivos
- **Exclusión de archivos**: Soporte para patrones de exclusión usando minimatch
- **Gestión de encoding**: Configuración flexible del encoding de archivos (UTF-8 por defecto)
### 2. Sistema de Watermarks
- **Persistencia de posición**: Mantiene registro de la última posición leída en cada archivo
- **Recuperación de estado**: Capacidad de continuar desde donde se dejó tras reinicio
- **Guardado automático**: Persiste watermarks cada 60 segundos
### 3. Modos de Lectura
- **Modo Offset**: Lectura basada en posición específica
- **Modo Watermark**: Lectura basada en marcas de agua persistentes
### 4. Monitoreo en Tiempo Real
- **Watch de directorios**: Monitoreo activo de nuevos archivos
- **Detección de cambios**: Identificación de modificaciones, truncamiento y rotación de archivos
## Configuración
### Parámetros de Configuración
```javascript
{
path: "ruta_a_archivos", // Expresión de ruta (usando jsexpr)
exclude: "patron_exclusion", // Patrón de archivos a excluir
readmode: "offset|watermark", // Modo de lectura
offset: "end|begin|start|numero", // Posición inicial de lectura
encoding: "utf8", // Codificación de archivos
watch: true|false, // Habilitar monitoreo
blocksize: 10240, // Tamaño del buffer de lectura
options: {} // Opciones adicionales
}
```
## Análisis de Código
### ✅ Aspectos Positivos
#### 1. **Gestión de Concurrencia Robusta**
```javascript
await sem.take(); // Adquiere semáforo
try {
// Operaciones críticas
} finally {
sem.leave(); // Libera semáforo
}
```
- Uso correcto de semáforos para evitar condiciones de carrera
- Protección de secciones críticas en operaciones de archivo
#### 2. **Manejo de Errores Consistente**
```javascript
try {
// Operaciones
} catch(err) {
logger.error(err);
file.sem.leave(); // Importante: libera recursos incluso en error
throw err;
}
```
#### 3. **Gestión de Recursos**
- Cierre automático de descriptores de archivo
- Limpieza de intervalos y monitores en `stop()`
- Gestión de memoria con listas de archivos
#### 4. **Flexibilidad de Configuración**
- Soporte para expresiones dinámicas en rutas
- Múltiples modos de lectura
- Configuración granular de comportamiento
#### 5. **Persistencia de Estado**
- Sistema de watermarks robusto
- Recuperación automática de estado
- Guardado periódico de progreso
### ⚠️ Posibles Problemas y Mejoras
#### 1. **Gestión de Memoria**
```javascript
// PROBLEMA: Acumulación potencial de líneas en memoria
file.lines.push({ln:file.line, line});
```
**Riesgo**: Archivos con líneas muy largas o muchas líneas pueden consumir mucha memoria.
**Solución recomendada**:
```javascript
// Implementar límite máximo de líneas en memoria
const MAX_LINES_IN_MEMORY = 1000;
if(file.lines.length >= MAX_LINES_IN_MEMORY) {
// Procesar o descartar líneas más antiguas
}
```
#### 2. **Manejo de Archivos Corruptos**
```javascript
// PROBLEMA: No hay validación de integridad de archivos
let res = await fs.read(file.fd,file.buffer,0,file.buffer.length,file.offset);
```
**Riesgo**: Archivos corruptos pueden causar comportamiento impredecible.
**Mejora sugerida**:
```javascript
try {
let res = await fs.read(file.fd,file.buffer,0,file.buffer.length,file.offset);
// Validar que res.bytesRead sea razonable
if(res.bytesRead < 0 || res.bytesRead > file.buffer.length) {
throw new Error(`Invalid bytes read: ${res.bytesRead}`);
}
} catch(err) {
logger.error(`Error reading file ${file.path}: ${err.message}`);
// Marcar archivo como problemático
}
```
#### 3. **Detección de Rotación de Archivos**
```javascript
// PROBLEMA: La detección de rotación solo verifica inode
if(fstat.ino != file.stats.ino) {
logger.warn(`File ${file.path} seems to be another file. Reseting reference.`);
}
```
**Limitación**: En algunos sistemas de archivos, el inode puede no cambiar.
**Mejora sugerida**:
```javascript
// Verificar múltiples propiedades del archivo
if(fstat.ino != file.stats.ino ||
fstat.mtime.getTime() < file.stats.mtime.getTime() ||
fstat.ctime.getTime() < file.stats.ctime.getTime()) {
logger.warn(`File ${file.path} appears to have been rotated or replaced`);
// Resetear referencia
}
```
#### 4. **Timeout en Operaciones de Archivo**
```javascript
// PROBLEMA: No hay timeout para operaciones de E/S
let fd = await fs.open(path, 'r');
```
**Riesgo**: Operaciones de archivo pueden colgarse indefinidamente.
**Solución**:
```javascript
const OPERATION_TIMEOUT = 30000; // 30 segundos
const withTimeout = (promise, timeout) => {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Operation timeout')), timeout)
)
]);
};
let fd = await withTimeout(fs.open(path, 'r'), OPERATION_TIMEOUT);
```
#### 5. **Validación de Configuración**
```javascript
// PROBLEMA: Falta validación robusta de configuración
async configure(config, callback) {
config = config || {};
this.path = jsexpr.expr(config.path);
// ...
}
```
**Mejora**:
```javascript
async configure(config, callback) {
if (!config) {
return callback(new Error('Configuration is required'));
}
if (!config.path) {
return callback(new Error('Path configuration is required'));
}
try {
this.path = jsexpr.expr(config.path);
} catch(err) {
return callback(new Error(`Invalid path expression: ${err.message}`));
}
// ...
}
```
#### 6. **Logging Excesivo**
```javascript
// PROBLEMA: Muchos logs en nivel silly pueden afectar rendimiento
logger.silly(`Reading ${file.path} from ${file.offset}`);
```
**Solución**: Implementar logging condicional o por niveles configurables.
### 🔧 Optimizaciones Sugeridas
#### 1. **Pool de Descriptores de Archivo**
```javascript
class FileDescriptorPool {
constructor(maxSize = MAX_OPEN) {
this.pool = new Map();
this.maxSize = maxSize;
this.lru = []; // Least Recently Used
}
async get(path) {
if (this.pool.has(path)) {
// Mover al final del LRU
this.lru = this.lru.filter(p => p !== path);
this.lru.push(path);
return this.pool.get(path);
}
// Si el pool está lleno, cerrar el menos usado
if (this.pool.size >= this.maxSize) {
const oldest = this.lru.shift();
const fd = this.pool.get(oldest);
await fs.close(fd);
this.pool.delete(oldest);
}
const fd = await fs.open(path, 'r');
this.pool.set(path, fd);
this.lru.push(path);
return fd;
}
}
```
#### 2. **Buffering Inteligente**
```javascript
// Ajustar tamaño de buffer basado en tamaño de archivo
calculateOptimalBufferSize(fileSize) {
if (fileSize < 1024 * 10) return 1024; // 1KB para archivos pequeños
if (fileSize < 1024 * 1024) return 1024 * 10; // 10KB para archivos medianos
return 1024 * 64; // 64KB para archivos grandes
}
```
#### 3. **Procesamiento Asíncrono Mejorado**
```javascript
// Usar async iterators para mejor manejo de flujo
async* readLinesIterator() {
while (!this.closed) {
const files = await this.readlines();
for (const file of files) {
while (file.lines.length > 0) {
yield this.formatLine(file.lines.shift(), file);
}
}
if (!this.hasAvailableLines()) {
await this.sleep(100); // Espera antes de siguiente iteración
}
}
}
```
## Patrones de Diseño Utilizados
### 1. **Observer Pattern**
- Monitoreo de archivos con eventos (`new`, `ready`)
- Callbacks para notificaciones de estado
### 2. **Strategy Pattern**
- Diferentes modos de lectura (offset vs watermark)
- Configuración flexible de comportamiento
### 3. **Resource Pool Pattern**
- Gestión de descriptores de archivo
- Control de recursos limitados
### 4. **State Pattern**
- Estados de archivo (ready, reading, closed)
- Transiciones controladas
## Casos de Uso Típicos
### 1. **Lectura de Logs en Tiempo Real**
```javascript
const fileInput = new FileInput('logs', 'file');
await fileInput.configure({
path: '/var/log/*.log',
watch: true,
readmode: 'watermark',
offset: 'end'
});
```
### 2. **Procesamiento de Archivos Históricos**
```javascript
const fileInput = new FileInput('batch', 'file');
await fileInput.configure({
path: '/data/historical/*.txt',
watch: false,
readmode: 'offset',
offset: 'begin'
});
```
### 3. **Monitoreo con Exclusiones**
```javascript
const fileInput = new FileInput('monitor', 'file');
await fileInput.configure({
path: '/logs/**/*.log',
exclude: '*/temp/*',
watch: true,
readmode: 'watermark'
});
```
## Análisis del Flujo de Ejecución
### Caso de Uso: Monitoreo de Logs con Estructura de Fechas
Para el caso específico de monitorizar archivos bajo la ruta `/ica/logroot/${YYYY-MM-DD}/*.log`, analizaremos el flujo completo de ejecución desde la inicialización hasta el procesamiento continuo.
#### Configuración del Caso de Uso
```javascript
const fileInput = new FileInput('daily-logs', 'file');
await fileInput.configure({
path: '/ica/logroot/${YYYY-MM-DD}/*.log',
watch: true,
readmode: 'watermark',
offset: 'end',
encoding: 'utf8',
blocksize: 65536, // 64KB para archivos de log grandes
options: {
persistent: true,
recursive: false
}
});
```
### 🔄 Flujo de Ejecución Detallado
#### **Fase 1: Inicialización (constructor + configure)**
```mermaid
graph TD
A[Constructor FileInput] --> B[Inicializar propiedades]
B --> C[configure() llamado]
C --> D[Parsear expresión de ruta con jsexpr]
D --> E[Configurar parámetros]
E --> F[Callback de configuración]
```
**1.1 Constructor**
```javascript
// Estado inicial
this.files = {}; // Mapa de archivos abiertos
this.list = { read: {}, avail: {} }; // Listas de control
this.sem = new Semaphore(1); // Control de concurrencia
this.watermark = null; // Sistema de marcas de agua
this.wm = null; // Datos de watermarks
this.monitors = new Map(); // Monitores de directorios
```
**1.2 Configuración**
```javascript
// La expresión jsexpr se evalúa dinámicamente
this.path = jsexpr.expr('/ica/logroot/${YYYY-MM-DD}/*.log');
// Resultado: función que genera rutas como "/ica/logroot/2025-07-08/*.log"
```
#### **Fase 2: Inicio del Sistema (start)**
```mermaid
graph TD
A[start() llamado] --> B[Inicializar Watermark]
B --> C[Cargar watermarks existentes]
C --> D{watch = true?}
D -->|Sí| E[Iniciar watchFiles]
D -->|No| F[Buscar archivos con glob]
E --> G[Configurar intervalo de monitoreo]
F --> H[Abrir archivos encontrados]
G --> I[Configurar guardado automático]
H --> I
I --> J[Sistema listo]
```
**2.1 Inicialización de Watermarks**
```javascript
try {
this.watermark = new Watermark(this.config.$datadir);
await this.watermark.start();
this.wm = await this.watermark.get(this.id); // Obtener watermarks de "daily-logs"
} catch(err) {
return callback(err);
}
```
**2.2 Inicio del Monitoreo**
```javascript
if(this.watch) {
await this.watchFiles(); // Primera ejecución
this.ivalWatch = setInterval(async()=>{
await this.watchFiles(); // Cada 5 segundos
}, 5000);
}
```
**2.3 Configuración de Guardado Automático**
```javascript
this.wmival = setInterval(this.save.bind(this), 60000); // Cada 60 segundos
```
#### **Fase 3: Monitoreo de Directorios (watchFiles)**
```mermaid
graph TD
A[watchFiles ejecutado] --> B[Evaluar expresión de ruta]
B --> C[Resolver ruta: /ica/logroot/2025-07-08]
C --> D{Monitor existe?}
D -->|No| E[Crear nuevo Monitor]
D -->|Sí| F[Continuar con monitor existente]
E --> G[Configurar eventos del monitor]
G --> H[monitor.start con opciones]
H --> I[Escuchar eventos 'new' y 'ready']
F --> I
```
**3.1 Evaluación Dinámica de Rutas**
```javascript
const path = slash(Path.resolve(config.$path, this.path("")));
// Resultado: "/ica/logroot/2025-07-08" (fecha actual)
```
**3.2 Creación del Monitor**
```javascript
if(!this.monitors.has(path)) {
const monitor = new Monitor(this.files);
this.monitors.set(path, monitor);
monitor.start(path, this.options);
// Eventos del monitor
monitor.on('new', path => this.openFile(path)); // Nuevo archivo detectado
monitor.on('ready', path => { // Archivo listo para lectura
this.list.read[path] = true;
});
}
```
#### **Fase 4: Detección y Apertura de Archivos (openFile)**
```mermaid
graph TD
A[Nuevo archivo detectado] --> B[openFile llamado]
B --> C{Archivo excluido?}
C -->|Sí| D[Ignorar archivo]
C -->|No| E[Adquirir semáforo]
E --> F{Archivo ya abierto?}
F -->|Sí| G[Retornar instancia existente]
F -->|No| H[Abrir descriptor de archivo]
H --> I[Obtener estadísticas del archivo]
I --> J[Determinar offset inicial]
J --> K[Crear instancia File]
K --> L[Actualizar estructuras de datos]
L --> M[Liberar semáforo]
```
**4.1 Exclusión de Archivos**
```javascript
// Para nuestro caso: /ica/logroot/2025-07-08/application.log
if (this.exclude && minimatch(path, this.exclude)) {
logger.info(`${path} is excluded`);
return;
}
```
**4.2 Determinación del Offset**
```javascript
if (this.readmode == MODE.watermark) {
if (wm[path]) {
// Archivo conocido - continuar desde watermark
offset = wm[path].offset || 0;
tail = wm[path].tail || "";
line = wm[path].line || 0;
lines = wm[path].lines || [];
} else {
// Archivo nuevo - empezar desde el final (offset: 'end')
offset = stats.size;
}
}
```
**4.3 Creación de la Instancia File**
```javascript
files[path] = new File(path, fd, stats, offset, buffer, tail, line, lines);
files[path].ready = true;
this.list.read[path] = true; // Marcar para lectura
extend(true, wm, { [path]: files[path].toJSON() });
```
#### **Fase 5: Procesamiento Continuo (next + readlines)**
```mermaid
graph TD
A[next() llamado] --> B[fetchLine()]
B --> C{Líneas disponibles?}
C -->|Sí| D[Retornar línea]
C -->|No| E[readlines()]
E --> F[Para cada archivo en lectura]
F --> G[Leer buffer del archivo]
G --> H[Procesar contenido]
H --> I[Dividir en líneas]
I --> J[Actualizar watermarks]
J --> K[fetchLine() nuevamente]
K --> L{Líneas disponibles?}
L -->|Sí| M[Retornar línea]
L -->|No| N[Retornar false]
```
**5.1 Lectura de Archivos (readlines)**
```javascript
// Para cada archivo marcado para lectura
let files = this.watch?
Object.keys(this.list.read).map(k=>this.files[k]) :
Object.keys(this.files).map(k=>this.files[k]);
// Lectura asíncrona paralela
let all = files.filter(Boolean).map(async(file)=>{
await this.openFile(file.path);
await file.sem.take();
// Leer desde la posición actual
let res = await fs.read(file.fd, file.buffer, 0, file.buffer.length, file.offset);
file.tail += res.buffer.toString('utf8', 0, res.bytesRead);
file.offset += res.bytesRead;
// Procesar líneas completas
let lines = file.tail.split("\n");
while(lines.length) {
let line = lines.shift();
if(lines.length) {
file.line++;
file.lines.push({ln:file.line, line});
} else {
file.tail = line; // Línea incompleta
}
}
file.sem.leave();
return file;
});
```
**5.2 Manejo de Casos Especiales**
```javascript
// Detección de truncamiento o rotación
if(res.bytesRead == 0) {
let fstat = await fs.stat(file.path);
if((fstat.size < file.offset) || (fstat.ino != file.stats.ino)) {
if(fstat.ino != file.stats.ino) {
logger.warn(`File ${file.path} seems to be another file. Reseting reference.`);
} else {
logger.warn(`File ${file.path} has been truncated. Reseting watermark`);
}
file.offset = 0; // Reiniciar desde el principio
// Cerrar y reabrir el archivo
}
}
```
#### **Fase 6: Entrega de Datos (fetchLine)**
```mermaid
graph TD
A[fetchLine llamado] --> B[Limpiar listas]
B --> C{Archivos disponibles?}
C -->|No| D[Retornar false]
C -->|Sí| E[Selección aleatoria de archivo]
E --> F[Extraer primera línea]
F --> G[Formar objeto de datos]
G --> H[Actualizar listas]
H --> I[Retornar datos]
```
**6.1 Estructura de Datos Retornada**
```javascript
// Para archivo: /ica/logroot/2025-07-08/application.log, línea 1542
let data = {
type: 'file',
path: '/ica/logroot/2025-07-08/application.log',
filename: 'application.log',
ln: 1542,
originalMessage: '2025-07-08 14:30:45 INFO [main] Application started successfully'
};
```
### 🕐 Cronología de Eventos para el Caso de Uso
#### **T+0s: Inicialización**
```
14:00:00 - Constructor FileInput('daily-logs', 'file')
14:00:00 - configure() con path '/ica/logroot/${YYYY-MM-DD}/*.log'
14:00:00 - start() iniciado
14:00:01 - Watermark cargado desde disco
14:00:01 - watchFiles() - monitorear /ica/logroot/2025-07-08/
```
#### **T+1s: Detección Inicial**
```
14:00:02 - Monitor detecta: application.log, error.log, access.log
14:00:02 - openFile(/ica/logroot/2025-07-08/application.log)
14:00:02 - openFile(/ica/logroot/2025-07-08/error.log)
14:00:02 - openFile(/ica/logroot/2025-07-08/access.log)
14:00:03 - Archivos listos para lectura
```
#### **T+5s: Primera Lectura**
```
14:00:05 - next() llamado por primera vez
14:00:05 - readlines() lee buffers de los 3 archivos
14:00:05 - 47 líneas procesadas de application.log
14:00:05 - 12 líneas procesadas de error.log
14:00:05 - 156 líneas procesadas de access.log
14:00:05 - fetchLine() retorna línea de access.log
```
#### **T+5s-60s: Procesamiento Continuo**
```
14:00:06 - next() → fetchLine() → línea de application.log
14:00:07 - next() → fetchLine() → línea de error.log
...
14:00:30 - Monitor detecta nuevo archivo: debug.log
14:00:30 - openFile(/ica/logroot/2025-07-08/debug.log)
...
14:01:00 - save() automático ejecutado
14:01:00 - Watermarks persistidos para 4 archivos
```
#### **T+24h: Rotación Diaria**
```
00:00:00 - watchFiles() evalúa nueva fecha
00:00:00 - Nueva ruta: /ica/logroot/2025-07-09/
00:00:01 - Nuevo monitor creado para directorio 2025-07-09
00:00:05 - Archivos del día anterior siguen siendo monitoreados
00:00:10 - Nuevos archivos detectados en directorio actual
```
### 📊 Métricas de Rendimiento del Flujo
#### **Latencia de Detección**
- **Nuevos archivos**: ~1-2 segundos (intervalo del monitor del filesystem)
- **Nuevas líneas**: ~0-100ms (dependiente de la frecuencia de `next()`
- **Rotación de archivos**: ~1-5 segundos
#### **Throughput**
- **Lectura secuencial**: ~10-50 MB/s (dependiente del disco)
- **Líneas por segundo**: ~1000-10000 (dependiente del tamaño de línea)
- **Archivos simultáneos**: Sin límite teórico (limitado por descriptores de archivo del SO)
#### **Uso de Memoria**
```javascript
// Estimación para el caso de uso
const memoryUsage = {
watermarks: '~1KB por archivo',
fileDescriptors: '~8KB por archivo abierto',
buffers: '64KB × número de archivos activos',
lineas_memoria: 'variable (riesgo de acumulación)'
};
```
### ⚠️ Consideraciones Específicas del Caso de Uso
#### **1. Gestión de Directorios por Fecha**
- **Ventaja**: Organización clara y rotación natural
- **Desafío**: Necesidad de múltiples monitores activos
- **Recomendación**: Limitar monitores a N días recientes
#### **2. Patrones de Crecimiento de Logs**
```javascript
// Patrón típico diario
const growthPattern = {
'00:00-06:00': 'Bajo (mantenimiento)',
'06:00-09:00': 'Creciente (inicio de actividad)',
'09:00-17:00': 'Alto (actividad normal)',
'17:00-00:00': 'Decreciente'
};
```
#### **3. Optimizaciones Específicas**
```javascript
// Configuración optimizada para logs diarios
const optimizedConfig = {
blocksize: 131072, // 128KB para archivos de log grandes
watch: true,
readmode: 'watermark',
offset: 'end',
options: {
persistent: true,
recursive: false, // No buscar en subdirectorios
awaitWriteFinish: { // Esperar que termine la escritura
stabilityThreshold: 2000,
pollInterval: 100
}
}
};
```
### 🔍 Puntos de Monitoreo y Debug
#### **Métricas Clave a Monitorear**
```javascript
const metrics = {
files_monitored: 'Número de archivos bajo monitoreo',
lines_processed: 'Líneas procesadas por segundo',
watermark_lag: 'Diferencia entre tamaño de archivo y watermark',
memory_usage: 'Uso de memoria del proceso',
file_descriptors: 'Número de descriptores abiertos',
monitor_directories: 'Directorios bajo monitoreo activo'
};
```
#### **Logs de Debug Útiles**
```javascript
// Habilitar en desarrollo
logger.level = 'silly';
// Logs específicos a buscar:
// "Found ${path} in watermarks" - Archivo conocido
// "Not found ${path} in watermarks" - Archivo nuevo
// "File ${path} has been truncated" - Rotación detectada
// "Reading ${file.path} from ${file.offset}" - Lectura activa
// "Nothing to read from ${file.path}" - Archivo sin cambios
```
Este análisis proporciona una visión completa del flujo de ejecución para el caso de uso específico, identificando los puntos críticos, optimizaciones posibles y métricas de rendimiento esperadas.
## Conclusiones
El módulo `FileInput` es una implementación robusta y bien estructurada para el manejo de archivos de entrada. Sus principales fortalezas incluyen:
1. **Gestión de concurrencia sólida** con semáforos
2. **Sistema de watermarks persistente** para recuperación de estado
3. **Flexibilidad de configuración** para diferentes casos de uso
4. **Monitoreo en tiempo real** de sistemas de archivos
Las áreas de mejora principales se centran en:
1. **Gestión de memoria** más estricta
2. **Validación y manejo de errores** más robustos
3. **Optimizaciones de rendimiento** para casos de alto volumen
4. **Métricas y monitoreo** integrados
En general, el código demuestra buenas prácticas de programación asíncrona en Node.js y proporciona una base sólida para un sistema de ingesta de archivos de nivel empresarial.
## Versión del Análisis
- **Fecha**: 8 de Julio, 2025
- **Archivo analizado**: `c:\opt\nsyslog\lib\input\file\index.js`
- **Líneas de código**: ~400+ líneas
- **Complejidad**: Media-Alta