UNPKG

journalctl-ts

Version:

A journalctl client for node, written in Typescript

248 lines 9.87 kB
/** * @author Thomas Novotny * @packageDocumentation */ import EventEmitter from "events"; import childProcess from "child_process"; import readLine from "readline"; /** * Validata a fieldname as proper journald fieldName. Allowed are only A-Z, 0-9 and underscores * @param fieldName Field name to validate * @throws Error if field name is invalid * @returns The same field name */ function validateFieldName(fieldName) { const RegExp_ValidFieldName = /^[A-Z0-9_]+$/; if (!RegExp_ValidFieldName.test(fieldName)) throw new Error(`Not a valid journald Field Name: "${fieldName}"`); return fieldName; } /** * Validate a string as proper syslog identifier * @param identifier String to validate * @throws Error if field syslog identifier is invalid * @returns The same identifier */ function validateSyslogIdentifier(identifier) { const RegExp_SyslogIdentifier = /^[^\s]+$/; if (!RegExp_SyslogIdentifier.test(identifier)) throw new Error(`Not a valid SYSLOG_IDENTIFIER: "${identifier}"`); return identifier; } /** * Validate date as proper date * @param dateString date string * @throws Error if date string is invalid * @returns well-formed date string */ function validateDate(dateString) { const RegExp_DateString = /^\d\d\d\d-\d\d-\d\d(?: \d\d:\d\d(?::\d\d)?)?$/; if (!RegExp_DateString.test(dateString)) throw new Error(`Not a valid date: "${dateString}"`); if (isNaN(new Date(dateString).getTime())) throw new Error(`Not a valid date: "${dateString}"`); return dateString; } /** * Validate that a value is a positive integer * @throws Error if value is either not an integer or not positive * @param value the value that was tested */ function validatePositiveInteger(value) { if (!Number.isInteger(value) || isNaN(value)) throw new Error(`Not an integer: ${value}`); if (value < 0) throw new Error(`Not a positive integer: ${value}`); return value; } /** * Validates a journald priority. * If priority is an integer, the functions clamps the values to the next valid priority value * @param priority priority value * @throws Error if priority is not a number or not an integer * @returns valid journald priority */ function validatePriority(priority) { validatePositiveInteger(priority); if (priority < 0 || priority > 7) throw new Error(`Not a valid priority: ${priority}`); return priority; } /** * Validates a systemd unit name * @param unit Unit name to validate * @throws Error if unit name is not valid * @returns The unit name */ function validateUnit(unit) { const RegExp_Systemd_Unit_Name = /^[A-Za-z0-9:\-_\.\\]+$/; if (!RegExp_Systemd_Unit_Name.test(unit)) throw new Error(`Not a valid systemd unit name: "${unit}"`); return unit; } /** * This class is an EventEmitter that watches the local systemd journal for events and emits them. * @typeParam FieldNames List of field names that should be included in the JournalD messages. If left undefined, the messages will include all available fields. */ export default class JournalCtl extends EventEmitter { /** The journalctl process instance */ journalCtl; /** stdErr */ stdErrorText; /** Tracks whether an asyncgenerator exists already */ generatorInstanceExtists; /** * Create a new JournalCtl interface with the specified options * * @param options Optional options object * @throws Error if options are malformed or if the journalctl process can not be spawned */ constructor(options = {}, outputFields) { super(); /** Initial args: * -q (no error output) * -o json (output as newline delimited json) */ //const args: string[] = ['-q', '-o', 'json']; const args = ['-o', 'json']; if (options.until) { let dateString = typeof (options.until) === 'string' ? validateDate(options.until) : options.until.toISOString().replace(/T|\.\d\d\dZ/g, ' ').trim(); args.push('-U', `${dateString}`); } else { // If "until" is not set, set -f (tail) args.push("-f"); } if (options.all) args.push('-a'); // Unless otherwise defined, default lines are 10 if (!options.until && !options.since && options.lines === undefined) { args.push('-n', `${10}`); } else if (options.lines !== undefined) { args.push('-n', `${validatePositiveInteger(options.lines)}`); } if (options.since) { let dateString = typeof (options.since) === 'string' ? validateDate(options.since) : options.since.toISOString().replace(/T|\.\d\d\dZ/g, ' ').trim(); args.push('-S', `${dateString}`); } if (options.since && options.until) { const dSince = (typeof (options.since) == 'string' ? new Date(options.since) : options.since); const dUntil = (typeof (options.until) == 'string' ? new Date(options.until) : options.until); if (dSince.getTime() > dUntil.getTime()) throw new Error(`'Since' date can not be more recent than 'Until' date.`); } if (options.priority !== undefined) { args.push('-p'); if (typeof (options.priority) === 'number') { args.push(`${validatePriority(options.priority)}`); } else { args.push(`${validatePriority(options.priority.from)}..${validatePriority(options.priority.to)}`); } } if (options.identifier !== undefined) args.push('-t', validateSyslogIdentifier(options.identifier)); if (options.unit) args.push('-u', validateUnit(options.unit)); if (options.filter) { for (let key in options.filter) { args.push(`${validateFieldName(key)}=${options.filter[key]}`); } } if (outputFields) { outputFields.forEach((fieldName) => { args.push('--output-field', `${validateFieldName(fieldName)}`); }); if (!outputFields.includes("MESSAGE")) args.push('--output-field', `MESSAGE`); if (!outputFields.includes("SYSLOG_IDENTIFIER")) args.push('--output-field', `SYSLOG_IDENTIFIER`); if (!outputFields.includes("_HOSTNAME")) args.push('--output-field', `_HOSTNAME`); } // Start journalctl //console.log(['journalctl', ...args].join(' ')); this.journalCtl = childProcess.spawn('journalctl', args); this.stdErrorText = ""; this.journalCtl.stderr.on('data', (chunk) => { this.stdErrorText += chunk.toString(); }); this.journalCtl.on("exit", (code) => { if (code) { this.emit('error', new Error(this.stdErrorText ?? `Process exited with code ${code}`)); } this.emit('exit'); }); this.journalCtl.on("error", (err) => { if (err.message.includes("ENOENT")) { this.emit('error', new Error(`journalctl executable not found. Is this a systemd distribution?`)); } else if (err.message.includes("EACCES")) { this.emit('error', new Error(`journalctl executable not accessible.`)); } else { this.emit('error', err); } }); const lineReader = readLine.createInterface(this.journalCtl.stdout); lineReader.on('line', (line) => { try { // Note: apparently journald can in some cases end messages like ", }" which adds an invalid comma. The replace function removes it let message = JSON.parse(line.replace(/,\s*\}$/, ' }')); if (message.MESSAGE === undefined) message.MESSAGE = ""; if (message._HOSTNAME === undefined) message._HOSTNAME = ""; this.emit('message', message); } catch (err) { this.emit('error', new Error(`Could not parse JSON: ${line}`)); } }); this.generatorInstanceExtists = false; } on(event, callBack) { return super.on(event, callBack); } /** * Creates an AsyncGenerator for this instance * @returns AsyncGenerator which yields one journald message at a time as they come in */ async *createGenerator() { if (this.generatorInstanceExtists) { throw new Error(`Only one instance of the async message generator can be created per JournalCtl instance.`); } this.generatorInstanceExtists = true; let totalEntries = 0; let resolve; //let reject: (err: Error) => void; const messagePromises = []; const createPromise = () => new Promise((res /*, rej */) => { resolve = res; /*reject = rej;*/ }); messagePromises.push(createPromise()); this.on('error', () => { //reject(err); resolve(undefined); }).on('message', message => { const oldResolve = resolve; messagePromises.push(createPromise()); totalEntries++; oldResolve(message); }).on('exit', () => { resolve(undefined); }); let result; while (result = await messagePromises.shift()) { yield result; } return { totalEntries: totalEntries }; } /** * Stops the journalctl process and causes this instance to eventually emit the 'exit' event * @returns the return value of ChildProcess.kill() */ stop() { return this.journalCtl.kill(); } } //# sourceMappingURL=JournalCtl.js.map