Optimiza el archivado de logs de CI con Node.js
Los sistemas de integración continua generan una cantidad enorme de datos de log, por lo que archivarlos de forma eficiente resulta esencial. Los streams de Node.js y las bibliotecas tar ofrecen una solución potente para procesar y archivar logs en tiempo real, minimizando la E/S de disco y mejorando el rendimiento.
Dependencias
Antes de empezar, asegúrate de tener instalado Node.js 22 o una versión más reciente. Crea un proyecto nuevo e instala las dependencias necesarias:
npm init -y
npm install tar-stream@3.1.7
Crear un archivo de logs en tiempo real
El siguiente ejemplo escribe un archivo tar en el destino indicado mientras agrega entradas. El contenido de cada log ya está en memoria, pero no es necesario almacenar en búfer el archivo tar completo. Consumir el stream de empaquetado mientras se producen las entradas es esencial: de lo contrario, los archivos tar más grandes pueden bloquearse por contrapresión antes de que quien llama reciba un stream.
const tar = require('tar-stream')
const path = require('node:path')
const { createWriteStream } = require('node:fs')
const { pipeline } = require('node:stream/promises')
function validateFilename(filename) {
if (
!filename || filename.includes('\0') ||
path.posix.isAbsolute(filename) || path.win32.isAbsolute(filename) ||
filename.includes(':') || filename.split(/[\\/]/).includes('..')
) {
throw new Error('Invalid archive filename')
}
return filename.replaceAll('\\', '/')
}
async function addLogToArchive(pack, filename, content) {
return new Promise((resolve, reject) => {
pack.entry({ name: validateFilename(filename) }, content, (err) => {
if (err) reject(err)
else resolve()
}).on('error', reject)
})
}
async function createLogArchive(logs, output, { signal } = {}) {
const pack = tar.pack()
const completed = pipeline(pack, output, { signal })
const producing = (async () => {
try {
for await (const log of logs) {
await addLogToArchive(pack, log.filename, log.content)
}
pack.finalize()
} catch (error) {
pack.destroy(error)
throw error
}
})()
await Promise.all([completed, producing])
}
// Example usage with error handling
async function archiveLogs(output) {
const logs = [
{
filename: 'build.log',
content: `Build started at ${new Date().toISOString()}
Installing dependencies...
Build completed successfully.`,
},
{
filename: 'test.log',
content: `Test suite initiated.
All tests passed without errors.
Execution time: 12s.`,
},
]
await createLogArchive(logs, output)
}
archiveLogs(createWriteStream('ci-logs.tar')).catch(() => {
console.error('Failed to create the log archive')
process.exitCode = 1
})
Consideraciones de rendimiento
Cuando trabajes con archivos tar en Node.js, ten presentes estas optimizaciones de rendimiento:
- Usa streams para archivos grandes y así minimizar el uso de memoria
- Considera usar worker threads para la compresión en escenarios de alto rendimiento
- Implementa el manejo de contrapresión para conjuntos de datos grandes
- Usa tamaños de búfer adecuados al trabajar con datos binarios
Consideraciones de seguridad
El helper validateFilename rechaza los segmentos de directorio padre, los bytes nulos, las
rutas absolutas tanto en Windows como en POSIX y los separadores de unidad o de flujo alternativo
antes de crear una entrada. No normalices los segmentos de recorrido antes de comprobarlos. Mantén
relativos los nombres dentro del archivo, usa contenido de confianza y aplica protecciones
independientes de rutas y enlaces simbólicos dondequiera que se extraigan los archivos.
Manejar distintos tipos de archivos
Al archivar diversos tipos de archivos, tendrás que tratarlos de la forma adecuada:
async function addFileToArchive(pack, filename, content, encoding = 'utf8') {
const options = {
name: validateFilename(filename),
// Set appropriate mode for executable files
mode: filename.endsWith('.sh') ? 0o755 : 0o644,
}
return new Promise((resolve, reject) => {
// Handle Buffer input for binary files
const data = Buffer.isBuffer(content) ? content : Buffer.from(content, encoding)
pack.entry(options, data, (err) => {
if (err) reject(err)
else resolve()
}).on('error', reject)
})
}
Solución de problemas comunes
Estas son las soluciones a los retos habituales al trabajar con streams tar:
- Fugas de memoria:
async function handleArchiveStream(archiveStream, outputStream) {
await pipeline(archiveStream, outputStream)
}
- Archivos incompletos:
async function createArchiveWithTimeout(logs, outputStream, timeoutMs = 30000) {
await createLogArchive(logs, outputStream, {
signal: AbortSignal.timeout(timeoutMs),
})
}
Esto aborta el pipeline del stream en lugar de limitarse a rechazar una promesa de temporizador independiente. Descarta la salida parcial después de un fallo. Si los logs provienen de una fuente asíncrona externa, esa fuente también debe observar la cancelación para no quedarse bloqueada esperando otro log.
Conclusión
Los streams de Node.js combinados con las bibliotecas tar ofrecen una solución eficiente para archivar logs en tiempo real en pipelines de CI. Este enfoque minimiza la E/S de disco y mejora el rendimiento, sin dejar de ser flexible para distintos casos de uso. Al implementar un manejo de errores y unas medidas de seguridad adecuados, puedes construir sistemas de archivado robustos y fiables para tu flujo de trabajo de desarrollo.
¡Feliz programación!
