diff --git a/web/packages/teleterm/src/main.ts b/web/packages/teleterm/src/main.ts index c9562598ab31..e7f1c1cf20a2 100644 --- a/web/packages/teleterm/src/main.ts +++ b/web/packages/teleterm/src/main.ts @@ -7,7 +7,7 @@ import { app, globalShortcut, shell } from 'electron'; import MainProcess from 'teleterm/mainProcess'; import { getRuntimeSettings } from 'teleterm/mainProcess/runtimeSettings'; import { enableWebHandlersProtection } from 'teleterm/mainProcess/protocolHandler'; -import createLoggerService from 'teleterm/services/logger'; +import { LoggerColor, createFileLoggerService } from 'teleterm/services/logger'; import Logger from 'teleterm/logger'; import * as types from 'teleterm/types'; import { ConfigServiceImpl } from 'teleterm/services/config'; @@ -145,10 +145,11 @@ function initializeApp(): void { } function initMainLogger(settings: types.RuntimeSettings) { - const service = createLoggerService({ + const service = createFileLoggerService({ dev: settings.dev, dir: settings.userDataDir, name: 'main', + loggerNameColor: LoggerColor.Magenta, }); Logger.init(service); diff --git a/web/packages/teleterm/src/mainProcess/mainProcess.ts b/web/packages/teleterm/src/mainProcess/mainProcess.ts index 5c0d55a8fd95..7153701273ab 100644 --- a/web/packages/teleterm/src/mainProcess/mainProcess.ts +++ b/web/packages/teleterm/src/mainProcess/mainProcess.ts @@ -5,16 +5,16 @@ import fs from 'fs/promises'; import { app, - ipcMain, - shell, dialog, + ipcMain, Menu, MenuItemConstructorOptions, + shell, } from 'electron'; import { FileStorage, Logger, RuntimeSettings } from 'teleterm/types'; import { subscribeToFileStorageEvents } from 'teleterm/services/fileStorage'; -import createLoggerService from 'teleterm/services/logger'; +import { LoggerColor, createFileLoggerService } from 'teleterm/services/logger'; import { ChildProcessAddresses } from 'teleterm/mainProcess/types'; import { getAssetPath } from 'teleterm/mainProcess/runtimeSettings'; @@ -80,7 +80,7 @@ export default class MainProcess { this.logger.info(`Starting tsh daemon from ${binaryPath}`); this.tshdProcess = spawn(binaryPath, flags, { - stdio: 'pipe', + stdio: 'pipe', // stdio must be set to `pipe` as the gRPC server address is read from stdout windowsHide: true, env: { ...process.env, @@ -88,15 +88,16 @@ export default class MainProcess { }, }); - const tshdLogger = createLoggerService({ + const tshdPassThroughLogger = createFileLoggerService({ dev: this.settings.dev, dir: this.settings.userDataDir, name: 'tshd', + loggerNameColor: LoggerColor.Cyan, passThroughMode: true, }); - tshdLogger.pipeProcessOutputIntoLogger(this.tshdProcess.stdout); - tshdLogger.pipeProcessOutputIntoLogger(this.tshdProcess.stderr); + tshdPassThroughLogger.pipeProcessOutputIntoLogger(this.tshdProcess.stdout); + tshdPassThroughLogger.pipeProcessOutputIntoLogger(this.tshdProcess.stderr); this.tshdProcess.on('error', error => { this.logger.error('tshd failed to start', error); @@ -112,9 +113,23 @@ export default class MainProcess { path.join(__dirname, 'sharedProcess.js'), [`--runtimeSettingsJson=${JSON.stringify(this.settings)}`], { - stdio: 'pipe', + stdio: 'pipe', // stdio must be set to `pipe` as the gRPC server address is read from stdout } ); + const sharedProcessPassThroughLogger = createFileLoggerService({ + dev: this.settings.dev, + dir: this.settings.userDataDir, + name: 'shared', + loggerNameColor: LoggerColor.Yellow, + passThroughMode: true, + }); + + sharedProcessPassThroughLogger.pipeProcessOutputIntoLogger( + this.sharedProcess.stdout + ); + sharedProcessPassThroughLogger.pipeProcessOutputIntoLogger( + this.sharedProcess.stderr + ); this.sharedProcess.on('error', error => { this.logger.error('shared process failed to start', error); diff --git a/web/packages/teleterm/src/preload.ts b/web/packages/teleterm/src/preload.ts index 836c488b83cc..7c2ceb5d688c 100644 --- a/web/packages/teleterm/src/preload.ts +++ b/web/packages/teleterm/src/preload.ts @@ -3,7 +3,7 @@ import { ChannelCredentials } from '@grpc/grpc-js'; import createTshClient from 'teleterm/services/tshd/createClient'; import createMainProcessClient from 'teleterm/mainProcess/mainProcessClient'; -import createLoggerService from 'teleterm/services/logger'; +import { createFileLoggerService } from 'teleterm/services/logger'; import Logger from 'teleterm/logger'; import { createPtyService } from 'teleterm/services/pty/ptyService'; import { @@ -18,7 +18,7 @@ import { ElectronGlobals, RuntimeSettings } from 'teleterm/types'; const mainProcessClient = createMainProcessClient(); const runtimeSettings = mainProcessClient.getRuntimeSettings(); -const loggerService = createLoggerService({ +const loggerService = createFileLoggerService({ dev: runtimeSettings.dev, dir: runtimeSettings.userDataDir, name: 'renderer', diff --git a/web/packages/teleterm/src/services/logger/index.ts b/web/packages/teleterm/src/services/logger/index.ts index f9fa18780370..862e2c4638af 100644 --- a/web/packages/teleterm/src/services/logger/index.ts +++ b/web/packages/teleterm/src/services/logger/index.ts @@ -1,2 +1 @@ -import createLoggerService from './loggerService'; -export default createLoggerService; +export * from './loggerService'; diff --git a/web/packages/teleterm/src/services/logger/loggerService.ts b/web/packages/teleterm/src/services/logger/loggerService.ts index 8d1f22f9d779..11dd47d5b0ff 100644 --- a/web/packages/teleterm/src/services/logger/loggerService.ts +++ b/web/packages/teleterm/src/services/logger/loggerService.ts @@ -1,11 +1,48 @@ -import { createLogger as createWinston, format, transports } from 'winston'; +import winston, { + createLogger as createWinston, + format, + transports, +} from 'winston'; import { isObject } from 'lodash'; import split2 from 'split2'; -import { Logger, NodeLoggerService } from './types'; +import { Logger, LoggerService, NodeLoggerService } from './types'; -export default function createLoggerService(opts: Options): NodeLoggerService { +/** + * stdout logger should be used in child processes. + * It sends logs directly to stdout, so the parent logger can process that output + * (e.g. show it in the terminal and save to a file). + * It also allows parent to log errors emitted by the child process. + */ +export function createStdoutLoggerService(): LoggerService { + const instance = createWinston({ + level: 'info', + exitOnError: false, + format: format.combine( + format.printf(({ level, message, context }) => { + const text = stringifier(message as unknown as unknown[]); + return `[${context}] ${level}: ${text}`; + }) + ), + transports: [new transports.Console()], + }); + + return { + createLogger(context = 'default'): Logger { + const logger = instance.child({ context }); + return createLoggerFromWinston(logger); + }, + }; +} + +/** + * File logger saves logs directly to the file and shows them in the terminal in dev mode. + * Can be used as a parent logger and process logs from child processes. + */ +export function createFileLoggerService( + opts: FileLoggerOptions +): NodeLoggerService { const instance = createWinston({ level: 'info', exitOnError: false, @@ -17,8 +54,8 @@ export default function createLoggerService(opts: Options): NodeLoggerService { const text = stringifier(message as unknown as unknown[]); const contextAndLevel = opts.passThroughMode ? '' - : ` [${context}] ${level}`; - return `[${timestamp}]${contextAndLevel}: ${text}`; + : ` [${context}] ${level}:`; + return `[${timestamp}]${contextAndLevel} ${text}`; }) ), transports: [ @@ -35,8 +72,16 @@ export default function createLoggerService(opts: Options): NodeLoggerService { instance.add( new transports.Console({ format: format.printf(({ level, message, context }) => { + const loggerName = + opts.loggerNameColor && + `\x1b[${opts.loggerNameColor}m${opts.name.toUpperCase()}\x1b[0m`; + const text = stringifier(message as unknown as unknown[]); - return opts.passThroughMode ? text : `[${context}] ${level}: ${text}`; + const logMessage = opts.passThroughMode + ? text + : `[${context}] ${level}: ${text}`; + + return [loggerName, logMessage].filter(Boolean).join(' '); }), }) ); @@ -50,17 +95,28 @@ export default function createLoggerService(opts: Options): NodeLoggerService { }, createLogger(context = 'default'): Logger { const logger = instance.child({ context }); - return { - error: (...args) => { - logger.error(args); - }, - warn: (...args) => { - logger.warn(args); - }, - info: (...args) => { - logger.info(args); - }, - }; + return createLoggerFromWinston(logger); + }, + }; +} + +// maps color names to ANSI colors +export enum LoggerColor { + Magenta = '45', + Cyan = '46', + Yellow = '43', +} + +function createLoggerFromWinston(logger: winston.Logger): Logger { + return { + error: (...args) => { + logger.error(args); + }, + warn: (...args) => { + logger.warn(args); + }, + info: (...args) => { + logger.info(args); }, }; } @@ -79,9 +135,15 @@ function stringifier(message: unknown[]): string { .join(' '); } -type Options = { +type FileLoggerOptions = { dir: string; name: string; + /** + * Specifies color for the logger name e.g. SHARED, TSHD. + * Logger name is printed in the terminal, only in dev mode. + * If not specified, the logger name will not be printed. + */ + loggerNameColor?: LoggerColor; dev?: boolean; /** * Mode for logger handling logs from other sources. Log level and context are not included in the log message. diff --git a/web/packages/teleterm/src/sharedProcess/sharedProcess.ts b/web/packages/teleterm/src/sharedProcess/sharedProcess.ts index abc5b40a34b4..f022d8e14769 100644 --- a/web/packages/teleterm/src/sharedProcess/sharedProcess.ts +++ b/web/packages/teleterm/src/sharedProcess/sharedProcess.ts @@ -1,6 +1,7 @@ import { Server, ServerCredentials } from '@grpc/grpc-js'; -import createLoggerService from 'teleterm/services/logger'; +import { createStdoutLoggerService } from 'teleterm/services/logger'; + import { createInsecureServerCredentials, createServerCredentials, @@ -16,7 +17,7 @@ import { PtyHostService } from './api/protogen/ptyHostService_grpc_pb'; import { createPtyHostService } from './ptyHost/ptyHostService'; const runtimeSettings = getRuntimeSettings(); -initializeLogger(runtimeSettings); +initializeLogger(); initializeServer(runtimeSettings); function getRuntimeSettings(): RuntimeSettings { @@ -34,12 +35,8 @@ function getRuntimeSettings(): RuntimeSettings { return runtimeSettings; } -function initializeLogger(runtimeSettings: RuntimeSettings): void { - const loggerService = createLoggerService({ - dev: runtimeSettings.dev, - dir: runtimeSettings.userDataDir, - name: 'shared', - }); +function initializeLogger(): void { + const loggerService = createStdoutLoggerService(); Logger.init(loggerService); const logger = new Logger();