CI-Log-Archivierung mit Node.js optimieren
Continuous-Integration-Systeme erzeugen enorme Mengen an Log-Daten, weshalb eine effiziente Log-Archivierung unverzichtbar ist. Node.js-Streams und tar-Bibliotheken bieten eine leistungsstarke Lösung, um Logs in Echtzeit zu verarbeiten und zu archivieren, den Disk-I/O zu minimieren und die Performance zu verbessern.
Abhängigkeiten
Bevor wir beginnen, stellen Sie sicher, dass Node.js 22 oder neuer installiert ist. Legen Sie ein neues Projekt an und installieren Sie die erforderlichen Abhängigkeiten:
npm init -y
npm install tar-stream@3.1.7
Ein Log-Archiv in Echtzeit erstellen
Das folgende Beispiel schreibt ein Archiv an ein übergebenes Ziel, während Einträge hinzugefügt werden. Der Inhalt jedes Logs liegt bereits im Speicher, das vollständige Archiv muss jedoch nicht gepuffert werden. Den Pack-Stream zu konsumieren, während Einträge erzeugt werden, ist entscheidend: Andernfalls können größere Archive durch Backpressure ins Stocken geraten, bevor der Aufrufer überhaupt einen Stream erhält.
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
})
Überlegungen zur Performance
Wenn Sie in Node.js mit tar-Archiven arbeiten, sollten Sie diese Performance-Optimierungen berücksichtigen:
- Verwenden Sie Streams für große Dateien, um den Speicherverbrauch zu minimieren
- Ziehen Sie Worker-Threads für die Komprimierung in Szenarien mit hohem Durchsatz in Betracht
- Implementieren Sie eine Backpressure-Behandlung für große Datensätze
- Verwenden Sie passende Puffergrößen im Umgang mit Binärdaten
Überlegungen zur Sicherheit
Die Hilfsfunktion validateFilename lehnt Segmente für übergeordnete Verzeichnisse,
Null-Bytes, absolute Pfade unter Windows wie unter POSIX sowie Laufwerks- und
Alternate-Stream-Trennzeichen ab, bevor ein Eintrag erstellt wird. Normalisieren Sie
Traversal-Segmente nicht weg, bevor Sie sie prüfen. Halten Sie Archivnamen relativ, verwenden Sie
vertrauenswürdige Inhalte und setzen Sie überall dort, wo Archive entpackt werden, separate
Schutzmaßnahmen für Pfade und Symlinks ein.
Unterschiedliche Dateitypen behandeln
Beim Archivieren verschiedener Dateitypen müssen Sie diese angemessen behandeln:
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)
})
}
Häufige Probleme beheben
Hier finden Sie Lösungen für häufige Herausforderungen beim Arbeiten mit tar-Streams:
- Speicherlecks:
async function handleArchiveStream(archiveStream, outputStream) {
await pipeline(archiveStream, outputStream)
}
- Unvollständige Archive:
async function createArchiveWithTimeout(logs, outputStream, timeoutMs = 30000) {
await createLogArchive(logs, outputStream, {
signal: AbortSignal.timeout(timeoutMs),
})
}
Dadurch wird die Stream-Pipeline abgebrochen, statt lediglich ein separates Timer-Promise abzulehnen. Verwerfen Sie nach einem Fehler die unvollständige Ausgabe. Stammen die Logs aus einer externen asynchronen Quelle, muss auch diese Quelle den Abbruch beachten, damit sie nicht blockiert auf ein weiteres Log wartet.
Fazit
Node.js-Streams in Kombination mit tar-Bibliotheken bieten eine effiziente Lösung für die Log-Archivierung in Echtzeit innerhalb von CI-Pipelines. Dieser Ansatz minimiert den Disk-I/O und steigert die Performance, während er zugleich flexibel für unterschiedliche Anwendungsfälle bleibt. Mit einer sauberen Fehlerbehandlung und geeigneten Sicherheitsmaßnahmen können Sie robuste und zuverlässige Archivierungssysteme für Ihren Entwicklungs-Workflow aufbauen.
Viel Spaß beim Programmieren!
