diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-01-12 13:30:46 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-01-15 13:32:09 +0100 |
| commit | fd02a14f3fad35f49cfd7b9deb3aae02fcf9dda2 (patch) | |
| tree | 1515c4cef215db8bc1487c50ccf30de06182eff3 | |
| parent | e0e9fe815b047fbc75e3bf01eea95133ae7c035f (diff) | |
| download | mullvadvpn-fd02a14f3fad35f49cfd7b9deb3aae02fcf9dda2.tar.xz mullvadvpn-fd02a14f3fad35f49cfd7b9deb3aae02fcf9dda2.zip | |
Implement logging functionality
| -rw-r--r-- | gui/src/main/logging.ts | 103 | ||||
| -rw-r--r-- | gui/src/renderer/lib/logging.ts | 10 | ||||
| -rw-r--r-- | gui/src/shared/logging-types.ts | 17 | ||||
| -rw-r--r-- | gui/src/shared/logging.ts | 120 |
4 files changed, 176 insertions, 74 deletions
diff --git a/gui/src/main/logging.ts b/gui/src/main/logging.ts new file mode 100644 index 0000000000..fb21c54b48 --- /dev/null +++ b/gui/src/main/logging.ts @@ -0,0 +1,103 @@ +import { app } from 'electron'; +import fs from 'fs'; +import path from 'path'; +import { IpcMainEventChannel } from '../shared/ipc-event-channel'; +import { LogLevel, ILogInput, ILogOutput } from '../shared/logging-types'; + +export const OLD_LOG_FILES = ['frontend-renderer.log']; + +export class FileOutput implements ILogOutput { + private fileDescriptor?: number; + + constructor(public level: LogLevel, private filePath: string) { + try { + this.fileDescriptor = fs.openSync(filePath, fs.constants.O_CREAT | fs.constants.O_WRONLY); + } catch (e) { + console.error(`Failed to open ${this.filePath}`); + } + } + + public dispose() { + if (this.fileDescriptor) { + try { + fs.closeSync(this.fileDescriptor); + } catch (e) { + console.error(`Failed to close ${this.filePath}`); + } + } + } + + public write(_level: LogLevel, message: string) { + if (this.fileDescriptor) { + fs.write(this.fileDescriptor, `${message}\n`, (err) => { + if (err) { + console.error(`Failed to log to ${this.filePath}`); + } + }); + } + } +} + +export class IpcInput implements ILogInput { + public on(handler: (level: LogLevel, message: string) => void) { + IpcMainEventChannel.logging.handleLog(({ level, message }) => handler(level, message)); + } +} + +export function getMainLogPath() { + return path.join(getLogDirectoryDir(), 'main.log'); +} + +export function getRendererLogPath() { + return path.join(getLogDirectoryDir(), 'renderer.log'); +} + +export function createLoggingDirectory(): void { + try { + fs.mkdirSync(getLogDirectoryDir(), { recursive: true }); + } catch (e) { + console.error('Failed to create logging directory'); + } +} + +// When cleaning up old log files they are first backed up and the next time removed. +export function cleanUpLogDirectory(fileNames: string[]): void { + fileNames.forEach((fileName) => { + const filePath = path.join(getLogDirectoryDir(), fileName); + rotateOrDeleteFile(filePath); + }); +} + +export function backupLogFile(filePath: string): boolean { + const backupFilePath = getBackupFilePath(filePath); + try { + fs.accessSync(filePath); + fs.renameSync(filePath, backupFilePath); + return true; + } catch (e) { + console.error(`Failed to backup ${filePath}`); + return false; + } +} + +export function rotateOrDeleteFile(filePath: string): void { + if (!backupLogFile(filePath)) { + const backupFilePath = getBackupFilePath(filePath); + try { + fs.accessSync(backupFilePath); + fs.unlinkSync(backupFilePath); + } catch (e) { + console.error(`Failed to delete ${filePath}`); + } + } +} + +function getBackupFilePath(filePath: string): string { + const parsedPath = path.parse(filePath); + parsedPath.base = parsedPath.name + '.old' + parsedPath.ext; + return path.normalize(path.format(parsedPath)); +} + +function getLogDirectoryDir() { + return app.getPath('logs'); +} diff --git a/gui/src/renderer/lib/logging.ts b/gui/src/renderer/lib/logging.ts new file mode 100644 index 0000000000..433ba20f8f --- /dev/null +++ b/gui/src/renderer/lib/logging.ts @@ -0,0 +1,10 @@ +import { IpcRendererEventChannel } from '../../shared/ipc-event-channel'; +import { ILogOutput, LogLevel } from '../../shared/logging-types'; + +export default class IpcOutput implements ILogOutput { + constructor(public level: LogLevel) {} + + public write(level: LogLevel, message: string) { + IpcRendererEventChannel.logging.log({ level: level, message }); + } +} diff --git a/gui/src/shared/logging-types.ts b/gui/src/shared/logging-types.ts new file mode 100644 index 0000000000..09bd3f66cf --- /dev/null +++ b/gui/src/shared/logging-types.ts @@ -0,0 +1,17 @@ +export enum LogLevel { + error, + warning, + info, + verbose, + debug, +} + +export interface ILogOutput { + level: LogLevel; + write(level: LogLevel, message: string): void; + dispose?(): void; +} + +export interface ILogInput { + on(handler: (level: LogLevel, message: string) => void): void; +} diff --git a/gui/src/shared/logging.ts b/gui/src/shared/logging.ts index 471fd758c8..48c290c9b5 100644 --- a/gui/src/shared/logging.ts +++ b/gui/src/shared/logging.ts @@ -1,91 +1,63 @@ -import { app, remote } from 'electron'; -import log from 'electron-log'; -import * as fs from 'fs'; -import * as path from 'path'; +import moment from 'moment'; +import { ILogInput, ILogOutput, LogLevel } from './logging-types'; -const LOG_FORMAT = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}][{level}] {text}'; +export class Logger { + private outputs: ILogOutput[] = []; -// Returns platform specific logs folder for application -// See open issue and PR on Github: -// 1. https://github.com/electron/electron/issues/10118 -// 2. https://github.com/electron/electron/pull/10191 -export function getLogsDirectory() { - const theApp = process.type === 'browser' ? app : remote.app; + public addOutput(output: ILogOutput) { + this.outputs.push(output); + } - switch (process.platform) { - case 'darwin': - // macOS: ~/Library/Logs/{appname} - return path.join(theApp.getPath('home'), 'Library/Logs', theApp.name); - default: - // Windows: %LOCALAPPDATA%\{appname}\logs - // Linux: ~/.config/{appname}/logs - return path.join(theApp.getPath('userData'), 'logs'); + public addInput(input: ILogInput) { + input.on((level: LogLevel, message: string) => this.outputMessage(level, message)); } -} -export function getMainLogFile(): string { - return path.join(getLogsDirectory(), 'frontend.log'); -} + public log(level: LogLevel, ...data: unknown[]) { + const time = moment().format('YYYY-MM-DD HH:mm:ss.SSS'); + const message = `[${time}][${LogLevel[level]}] ${data.join(' ')}`; -export function getRendererLogFile(): string { - return path.join(getLogsDirectory(), 'renderer.log'); -} - -export function setupLogging(logFile: string) { - log.transports.console.format = LOG_FORMAT; - log.transports.file.format = LOG_FORMAT; - log.transports.console.level = 'debug'; + this.outputMessage(level, message); + } - if (process.env.NODE_ENV === 'development') { - // Disable log file in development - log.transports.file.level = false; - } else { - // Configure logging to file - log.transports.file.level = 'debug'; - log.transports.file.resolvePath = (_variables) => logFile; + public error = (...data: unknown[]) => this.log(LogLevel.error, ...data); + public warn = (...data: unknown[]) => this.log(LogLevel.warning, ...data); + public info = (...data: unknown[]) => this.log(LogLevel.info, ...data); + public verbose = (...data: unknown[]) => this.log(LogLevel.verbose, ...data); + public debug = (...data: unknown[]) => this.log(LogLevel.debug, ...data); - log.debug(`Logging to ${logFile}`); + public dispose() { + this.outputs.forEach((output) => output.dispose?.()); } -} -export function backupLogFile(filePath: string): boolean { - const exists = fileExists(filePath); - if (exists) { - const backupFilePath = getBackupFilePath(filePath); - fs.renameSync(filePath, backupFilePath); + private outputMessage(level: LogLevel, message: string) { + this.outputs + .filter((output) => level <= output.level) + .forEach((output) => output.write(level, message)); } - - return exists; } -function getBackupFilePath(filePath: string): string { - const parsedPath = path.parse(filePath); - parsedPath.base = parsedPath.name + '.old' + parsedPath.ext; - return path.normalize(path.format(parsedPath)); -} +export class ConsoleOutput implements ILogOutput { + constructor(public level: LogLevel) {} -function fileExists(filePath: string): boolean { - try { - fs.accessSync(filePath); - return true; - } catch { - return false; + public write(level: LogLevel, message: string) { + switch (level) { + case LogLevel.error: + console.error(message); + break; + case LogLevel.warning: + console.warn(message); + break; + case LogLevel.info: + console.info(message); + break; + case LogLevel.verbose: + console.log(message); + break; + case LogLevel.debug: + console.debug(message); + break; + } } } -// When cleaning up old log files they are first backed up and the next time removed. -export function cleanUpLogDirectory(): void { - const oldLogFileNames = ['frontend-renderer.log']; - - oldLogFileNames.forEach((fileName) => { - const filePath = path.join(getLogsDirectory(), fileName); - rotateOrDeleteFile(filePath); - }); -} - -export function rotateOrDeleteFile(filePath: string) { - const backupFilePath = getBackupFilePath(filePath); - if (!backupLogFile(filePath) && fileExists(backupFilePath)) { - fs.unlinkSync(backupFilePath); - } -} +export default new Logger(); |
