Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IDE's logging to a file #6478

Merged
merged 9 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/ide-desktop/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const DEFAULT_IMPORT_ONLY_MODULES =
const ALLOWED_DEFAULT_IMPORT_MODULES = `${DEFAULT_IMPORT_ONLY_MODULES}|postcss|react-hot-toast`
const OUR_MODULES = 'enso-content-config|enso-common'
const RELATIVE_MODULES =
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|file-associations|index|ipc|naming|paths|preload|security|url-associations'
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|file-associations|index|ipc|log|naming|paths|preload|security|url-associations'
const STRING_LITERAL = ':matches(Literal[raw=/^["\']/], TemplateLiteral)'
const JSX = ':matches(JSXElement, JSXFragment)'
const NOT_PASCAL_CASE = '/^(?!_?([A-Z][a-z0-9]*)+$)/'
Expand Down
2 changes: 2 additions & 0 deletions app/ide-desktop/lib/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as debug from 'debug'
// eslint-disable-next-line no-restricted-syntax
import * as fileAssociations from 'file-associations'
import * as ipc from 'ipc'
import * as log from 'log'
import * as naming from 'naming'
import * as paths from 'paths'
import * as projectManager from 'bin/project-manager'
Expand All @@ -45,6 +46,7 @@ class App {
isQuitting = false

async run() {
log.addFileLog()
urlAssociations.registerAssociations()
// Register file associations for macOS.
fileAssociations.setOpenFileEventHandler(id => {
Expand Down
98 changes: 98 additions & 0 deletions app/ide-desktop/lib/client/src/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/** @file Logging utilities.
*
* This module includes a special {@link addFileLog function} that adds a new log consumer that writes to a file.
*
* This is the primary entry point, though its building blocks are also exported, like {@link FileConsumer}. */

import * as fsSync from 'node:fs'
import * as pathModule from 'node:path'

import * as contentConfig from 'enso-content-config'
import * as paths from 'paths'

import * as linkedDist from '../../../../../target/ensogl-pack/linked-dist'

// ================
// === Log File ===
// ================

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sections

/** Adds a new log consumer that writes to a file.
*
* The path of the log file is {@link generateUniqueLogFileName automatically generated}.
*
* The log file is created in the {@link paths.LOGS_DIRECTORY logs directory}
*
* @returns The full path of the log file. */
export function addFileLog(): string {
const dirname = paths.LOGS_DIRECTORY
const filename = generateUniqueLogFileName()
const logFilePath = pathModule.join(dirname, filename)
const consumer = new FileConsumer(logFilePath)
contentConfig.logger.addConsumer(consumer)
return logFilePath
}

/** Generate a unique log file name based on the current timestamp.
*
* @returns The file name log file. */
export function generateUniqueLogFileName(): string {
// Replace ':' with '-' because ':' is not allowed in file names.
const timestamp = new Date().toISOString().replace(/:/g, '-')
const version = contentConfig.VERSION.ide.raw
return `${timestamp}-ide-${version}.log`
}

// ================
// === Consumer ===
// ================

/** Log consumer that writes to a file. */
export class FileConsumer extends linkedDist.Consumer {
private readonly logFilePath: string
private readonly logFileHandle: number

/** Create a log consumer that writes to a file.
*
* @param logPath - The path of the log file. Must be writeable. */
constructor(logPath: string) {
super()
// Create the directory if it doesn't exist, otherwise fsSync.openSync will fail.
const logsDirectory = pathModule.dirname(logPath)
fsSync.mkdirSync(logsDirectory, { recursive: true })
this.logFilePath = logPath
this.logFileHandle = fsSync.openSync(this.logFilePath, 'a')
}

override message(level: linkedDist.LogLevel, ...args: unknown[]): void {
const timestamp = new Date().toISOString()
const message = args
.map(arg => (typeof arg === 'string' ? arg : JSON.stringify(arg)))
.join(' ')
const timestampedMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`

if (this.logFileHandle) {
try {
fsSync.writeSync(this.logFileHandle, timestampedMessage)
} catch (error) {
console.error('Failed to write log:', error)
}
} else {
// This should never happen, as the log file handle is initialized in the constructor.
console.error('Log file not initialized.')
}
}

override startGroup(...args: unknown[]): void {
this.message('log', '[GROUP START]', ...args)
}

override startGroupCollapsed(...args: unknown[]): void {
// We don't have a way to collapse groups in the file logger, so we just use the same
// function as startGroup.
this.message('log', '[GROUP START]', ...args)
}

override groupEnd(...args: unknown[]): void {
this.message('log', '[GROUP END]', ...args)
}
}
7 changes: 7 additions & 0 deletions app/ide-desktop/lib/client/src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import * as paths from '../paths'
*/
export const APP_PATH = electron.app.getAppPath()

/**
* Get the path of the directory where the log files of IDE are stored.
*
* This is based on the Electron `logs` directory, see {@link Electron.App.getPath}.
*/
export const LOGS_DIRECTORY = electron.app.getPath('logs')

/** The application assets, all files bundled with it. */
export const ASSETS_PATH = path.join(APP_PATH, 'assets')

Expand Down