ipsambeatae
Version:
Shared dependencies of Compass, the MongoDB extension for VSCode and MongoSH
143 lines (134 loc) • 3.85 kB
text/typescript
import type { Readable } from 'stream';
import { PassThrough } from 'stream';
import { createInterface as createReadlineInterface } from 'readline';
export async function* createLogEntryIterator(
stdout: Readable
): AsyncIterable<LogEntry> {
for await (const line of createReadlineInterface({ input: stdout })) {
if (!line.trim()) {
continue;
}
try {
const logEntry = parseAnyLogEntry(line);
yield logEntry;
} catch (error: any) {
break;
}
}
}
/** A parsed MongoDB log entry. */
export type LogEntry = {
timestamp: string;
severity: string;
component: string;
context: string;
message: string;
id: number | undefined;
attr: any;
};
/**
* Parse a log line from mongod < 4.4, i.e. before structured logging came into
* existence. You may have seen code like this before. :)
*
* @param line The MongoDB logv1 line.
* @returns The parsed line information.
*/
function parseOldLogEntry(line: string): LogEntry {
const re =
/^(?<timestamp>\S*) *(?<severity>\S*) *(?<component>\S*) *\[(?<context>[^\]]+)\]\s*(?<message>.*)$/;
const match = re.exec(line.trim());
if (!match) {
throw new Error(`Could not parse line ${JSON.stringify(line)}`);
}
return match.groups as unknown as LogEntry;
}
/**
* Parse a JSON (logv2) or legacy (logv1) log message.
*
* @param line The MongoDB log line.
* @returns The parsed line information.
*/
export function parseAnyLogEntry(line: string): LogEntry {
try {
const newFormat = JSON.parse(line);
return {
id: newFormat.id,
timestamp: newFormat.t?.$date,
severity: newFormat.s,
component: newFormat.c,
context: newFormat.ctx,
message: newFormat.msg,
attr: newFormat.attr,
};
} catch {
return parseOldLogEntry(line);
}
}
export function isFailureToSetupListener(entry: LogEntry): boolean {
return (
entry.id === 22856 /* Error setting up listener */ ||
entry.message.includes('Failed to set up listener')
);
}
/**
* Look at a log entry to figure out whether we are listening on a
* TCP port.
*
* @param logEntry A parsed mongodb log line.
* @returns The port in question, or an -1 if the log line did not match.
*/
function getPortFromLogEntry(logEntry: LogEntry): number {
let match;
// Log message id 23016 has the format
// { t: <timestamp>, s: 'I', c: 'NETWORK', id: 23016, ctx: 'listener', msg: '...', attr: { port: 27020 } }
if (logEntry.id === 23016) {
return logEntry.attr.port;
}
// Or, 4.2-style: <timestamp> I NETWORK [listener] waiting for connections on port 27020
if (
logEntry.id === undefined &&
(match = /^waiting for connections on port (?<port>\d+)$/i.exec(
logEntry.message
))
) {
return +(match.groups?.port ?? '0');
}
if (isFailureToSetupListener(logEntry)) {
throw new Error(
`Failed to setup listener (${logEntry.message} ${JSON.stringify(
logEntry.attr
)})`
);
}
return -1;
}
/**
* Go through a stream of parsed log entry objects and return the port/path
* data once found.
*
* @input A mongodb logv2/logv1 stream.
* @returns The port that the target process is listening on.
*/
export async function filterLogStreamForPort(
input: Readable
): Promise<{ port: number }> {
let port = -1;
const inputDuplicate = input.pipe(new PassThrough({ objectMode: true }));
try {
for await (const logEntry of inputDuplicate as AsyncIterable<LogEntry>) {
if (
logEntry.component !== 'NETWORK' ||
!['initandlisten', 'listener'].includes(logEntry.context)
) {
continue; // We are only interested in listening network events
}
port = getPortFromLogEntry(logEntry);
if (port !== -1) {
break;
}
}
} finally {
input.unpipe(inputDuplicate);
}
return { port };
}