Skip to content

Commit

Permalink
feat: improve logger (#108)
Browse files Browse the repository at this point in the history
* feat: use custom logger levels

* feat: add logger colors

* feat: use label format

* feat: customize log method singature

* feat: improve logger format performance
  • Loading branch information
yifanwww authored Feb 17, 2024
1 parent 7c63327 commit c19e4a7
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 37 deletions.
1 change: 1 addition & 0 deletions packages/app-main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
"dependencies": {
"@ter/app-common": "workspace:*",
"chalk": "^5.3.0",
"dayjs": "^1.11.9",
"electron-devtools-installer": "^3.2.0",
"electron-store": "^8.1.0",
Expand Down
5 changes: 3 additions & 2 deletions packages/app-main/src/main/apis/logger/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { LoggerMainAPI } from '@ter/app-common/apis/logger';
import { LoggerAPIKey } from '@ter/app-common/apis/logger';
import type { IpcMain } from 'electron';
import type winston from 'winston';

export function registerLoggerHandlers(ipc: IpcMain, logger: winston.Logger) {
import type { AppLogger } from 'src/main/logger';

export function registerLoggerHandlers(ipc: IpcMain, logger: AppLogger) {
const handlers: LoggerMainAPI = {
handleLog: (_, level, message, ...meta) => void logger.log(level, message, ...meta),

Expand Down
12 changes: 6 additions & 6 deletions packages/app-main/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { app, BrowserWindow } from 'electron';

import { registerAppGlobalHandlers } from './apis/app';
import { AppInfo } from './appInfo';
import { AppLogger } from './logger';
import { AppLoggerService } from './logger';
import { WindowManager } from './window';

async function installExtensions(): Promise<void> {
Expand All @@ -16,11 +16,11 @@ async function installExtensions(): Promise<void> {
} = await import(/* webpackChunkName: 'electron-devtools-installer' */ 'electron-devtools-installer');

const succeed = (name: string) => {
AppLogger.INSTANCE.info(`Added extension "${name}"`);
AppLoggerService.INSTANCE.info(`Added extension "${name}"`);
};

const fail = (err: unknown) => {
AppLogger.INSTANCE.error('An error occurred when install extension:', err);
AppLoggerService.INSTANCE.error('An error occurred when install extension:', err);
};

await Promise.all([
Expand All @@ -37,15 +37,15 @@ async function handleReady() {
AppInfo.init();
initThirdPartyModules();

AppLogger.INSTANCE.info('App ready.');
AppLoggerService.INSTANCE.info('App ready.');

if (process.env.NODE_ENV === 'development') {
await installExtensions();
}

registerAppGlobalHandlers();

AppLogger.INSTANCE.info('Registered event handlers.');
AppLoggerService.INSTANCE.info('Registered event handlers.');

WindowManager.INSTANCE.createWindow({ type: WindowType.MAIN });
}
Expand All @@ -59,7 +59,7 @@ app.on('window-all-closed', () => {
// On macOS, most applications and their menu bars will stay active unless users use `cmd + Q` to quit.
if (process.platform !== 'darwin') {
app.quit();
AppLogger.INSTANCE.info('App quited.');
AppLoggerService.INSTANCE.info('App quited.');
}
});

Expand Down
140 changes: 115 additions & 25 deletions packages/app-main/src/main/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ArrayUtil } from '@ter/app-common/utils';
import type { ForegroundColorName } from 'chalk';
import chalk from 'chalk';
import dayjs from 'dayjs';
import { app } from 'electron';
import nodePath from 'node:path';
Expand All @@ -7,10 +9,48 @@ import winston from 'winston';

import { AppInfo } from './appInfo';

// reference: https://mifi.no/blog/winston-electron-logger/
type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'verbose' | 'debug';

interface ILogObject {
message: string;
[key: string]: unknown;
}

interface AppLogMethod<TLogger> {
(message: string | ILogObject): TLogger;
(message: string, ...meta: unknown[]): TLogger;
}

export interface AppLogger extends Omit<winston.Logger, LogLevel> {
// The levels we support
fatal: AppLogMethod<AppLogger>;
error: AppLogMethod<AppLogger>;
warn: AppLogMethod<AppLogger>;
info: AppLogMethod<AppLogger>;
verbose: AppLogMethod<AppLogger>;
debug: AppLogMethod<AppLogger>;

// The built-in levels we don't support
help: never;
data: never;
prompt: never;
http: never;
input: never;
silly: never;
emerg: never;
alert: never;
crit: never;
warning: never;
notice: never;
}

function getLogFileName() {
const timeStr = dayjs.utc(AppInfo.INSTANCE.startedTime).format('YYYYMMDDTHHmmssSSS');
return nodePath.join(!app.isPackaged ? '.' : AppInfo.INSTANCE.useDataPath, 'logs', `app-${timeStr}.log`);
}

// https://github.com/winstonjs/winston/issues/1427
function combineMessageAndSplat(): winston.Logform.Format {
function splat(): winston.Logform.Format {
return {
transform: (info) => {
const args = (info[Symbol.for('splat')] ?? []) as unknown[];
Expand All @@ -22,46 +62,96 @@ function combineMessageAndSplat(): winston.Logform.Format {
};
}

export class AppLogger {
interface TypedTransformableInfo extends winston.Logform.TransformableInfo {
label: string;
timestamp: string;
}

function format(): winston.Logform.Format {
return winston.format.printf((_info) => {
const info = _info as TypedTransformableInfo;

const levelStr = info.level.toUpperCase().padStart(7);
const contextStr = `[${info.label}]`;
const messageStr = String(info.message);

return [info.timestamp, levelStr, contextStr, messageStr].join(' ');
});
}

function formatColorfully(colors: Record<string, ForegroundColorName>): winston.Logform.Format {
return winston.format.printf((_info) => {
const info = _info as TypedTransformableInfo;

const color = colors[info.level];

const levelStr = info.level.toUpperCase().padStart(7);
const contextStr = `[${info.label}]`;
const messageStr = String(info.message);

return [info.timestamp, chalk[color](levelStr), chalk.yellow(contextStr), chalk[color](messageStr)].join(' ');
});
}

export class AppLoggerService {
/**
* The instance for electron main process to log logs.
*/
private static _instance?: winston.Logger;
private static _instance?: AppLogger;

/**
* The instance for electron main process to log logs.
*/
static get INSTANCE(): winston.Logger {
if (!AppLogger._instance) {
AppLogger._instance = AppLogger.createLogger();
static get INSTANCE(): AppLogger {
if (!AppLoggerService._instance) {
AppLoggerService._instance = AppLoggerService.createLogger('Application');
}
return AppLogger._instance;
return AppLoggerService._instance;
}

private static _getLogFileName() {
const timeStr = dayjs.utc(AppInfo.INSTANCE.startedTime).format('YYYYMMDDTHHmmssSSS');
return nodePath.join(!app.isPackaged ? '.' : AppInfo.INSTANCE.useDataPath, 'logs', `app-${timeStr}.log`);
}
static createLogger(context: string): AppLogger {
const levels: Record<LogLevel, number> = {
fatal: 0,
error: 1,
warn: 2,
info: 3,
verbose: 4,
debug: 5,
};

const colors: Record<LogLevel, ForegroundColorName> = {
fatal: 'magentaBright',
error: 'red',
warn: 'yellow',
info: 'green',
verbose: 'cyan',
debug: 'blue',
};

const labelFormat = winston.format.label({ label: context });
const timestampFormat = winston.format.timestamp({ format: 'YYYY-MM-DD, HH:mm:ss.SSS' });
const splatFormat = splat();

static createLogger(label?: string): winston.Logger {
return winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
combineMessageAndSplat(),
winston.format.printf((info) =>
label
? `[${String(info.timestamp)}] (${label}) [${info.level}]: ${String(info.message)}`
: `[${String(info.timestamp)}] [${info.level}]: ${String(info.message)}`,
),
),
transports: ArrayUtil.filterFalsy([
new winston.transports.File({
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
filename: AppLogger._getLogFileName(),
filename: getLogFileName(),
options: { flags: 'a' },
format: winston.format.combine(labelFormat, timestampFormat, splatFormat, format()),
}),
!app.isPackaged && new winston.transports.Console(),
!app.isPackaged &&
new winston.transports.Console({
level: 'debug',
format: winston.format.combine(
labelFormat,
timestampFormat,
splatFormat,
formatColorfully(colors),
),
}),
]),
});
levels,
}) as AppLogger;
}
}
8 changes: 4 additions & 4 deletions packages/app-main/src/main/window/abstractWindow.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { WindowType } from '@ter/app-common/apis/app';
import { BrowserWindow, shell } from 'electron';
import path from 'node:path';
import type winston from 'winston';

import { registerLoggerHandlers } from '../apis/logger';
import { AppInfo } from '../appInfo';
import { WindowStateKeeper } from '../configuration';
import { AppLogger } from '../logger';
import type { AppLogger } from '../logger';
import { AppLoggerService } from '../logger';

import type { AbstractWindowOption, CloseWindowOption } from './types';

export abstract class AbstractWindow {
protected readonly _window: BrowserWindow;
protected readonly _windowType: WindowType;

protected readonly _logger: winston.Logger;
protected readonly _logger: AppLogger;

private readonly _onClose: (option: CloseWindowOption) => void | Promise<void>;

Expand All @@ -38,7 +38,7 @@ export abstract class AbstractWindow {
if (windowStateKeeper.fullScreen) this._window.setFullScreen(true);
windowStateKeeper.registerHandlers(this._window);

this._logger = AppLogger.createLogger(`${this._windowType}-${this.id}`);
this._logger = AppLoggerService.createLogger(`${this._windowType}-${this.id}`);

this._onClose = option.onClose;

Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c19e4a7

Please sign in to comment.