From 3f23de6c28820adfca220cbb600af5f26eba8e88 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 10 Apr 2024 14:35:15 +0700 Subject: [PATCH] feat: move log into monitoring extension (#2662) --- core/src/node/api/processors/app.ts | 16 +- .../node/api/restful/helper/startStopModel.ts | 36 +++-- core/src/node/helper/config.ts | 28 ---- core/src/node/helper/index.ts | 2 +- core/src/node/helper/log.ts | 37 ----- core/src/node/helper/logger.ts | 81 ++++++++++ core/src/node/helper/resource.ts | 5 +- .../types/monitoring/monitoringInterface.ts | 12 ++ electron/main.ts | 3 - electron/utils/log.ts | 67 --------- extensions/assistant-extension/README.md | 44 +++--- extensions/huggingface-extension/README.md | 40 ++--- extensions/inference-groq-extension/README.md | 45 +++--- .../inference-nitro-extension/README.md | 44 +++--- .../README.md | 45 +++--- extensions/model-extension/README.md | 45 +++--- extensions/monitoring-extension/README.md | 45 +++--- .../resources/settings.json | 21 +++ .../monitoring-extension/rollup.config.ts | 2 + extensions/monitoring-extension/src/index.ts | 44 +++++- .../monitoring-extension/src/node/index.ts | 21 +++ .../monitoring-extension/src/node/logger.ts | 138 ++++++++++++++++++ server/helpers/logger.ts | 35 +++++ server/index.ts | 29 ++-- web/containers/ServerLogs/index.tsx | 6 +- web/extension/ExtensionManager.ts | 27 +++- web/hooks/useLogs.tsx | 4 +- .../CoreExtensions/TensorRtExtensionItem.tsx | 6 +- .../Settings/ExtensionSetting/index.tsx | 5 +- .../SettingDetailToggleItem/index.tsx | 47 ++++++ .../SettingDetail/SettingDetailItem/index.tsx | 11 ++ web/screens/Settings/SettingMenu/index.tsx | 17 +-- 32 files changed, 635 insertions(+), 373 deletions(-) delete mode 100644 core/src/node/helper/log.ts create mode 100644 core/src/node/helper/logger.ts delete mode 100644 electron/utils/log.ts create mode 100644 extensions/monitoring-extension/resources/settings.json create mode 100644 extensions/monitoring-extension/src/node/logger.ts create mode 100644 server/helpers/logger.ts create mode 100644 web/screens/Settings/SettingDetail/SettingDetailItem/SettingDetailToggleItem/index.tsx diff --git a/core/src/node/api/processors/app.ts b/core/src/node/api/processors/app.ts index 28c5b7f58a..c98060da49 100644 --- a/core/src/node/api/processors/app.ts +++ b/core/src/node/api/processors/app.ts @@ -1,9 +1,12 @@ import { basename, isAbsolute, join, relative } from 'path' import { Processor } from './Processor' -import { getAppConfigurations as appConfiguration, updateAppConfiguration } from '../../helper' -import { log as writeLog, logServer as writeServerLog } from '../../helper/log' -import { appResourcePath } from '../../helper/path' +import { + log as writeLog, + appResourcePath, + getAppConfigurations as appConfiguration, + updateAppConfiguration, +} from '../../helper' export class App implements Processor { observer?: Function @@ -56,13 +59,6 @@ export class App implements Processor { writeLog(args) } - /** - * Log message to log file. - */ - logServer(args: any) { - writeServerLog(args) - } - getAppConfigurations() { return appConfiguration() } diff --git a/core/src/node/api/restful/helper/startStopModel.ts b/core/src/node/api/restful/helper/startStopModel.ts index d9c3291004..3af0404e37 100644 --- a/core/src/node/api/restful/helper/startStopModel.ts +++ b/core/src/node/api/restful/helper/startStopModel.ts @@ -1,7 +1,11 @@ import fs from 'fs' import { join } from 'path' -import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../../helper' -import { logServer } from '../../../helper/log' +import { + getJanDataFolderPath, + getJanExtensionsPath, + getSystemResourceInfo, + log, +} from '../../../helper' import { ChildProcessWithoutNullStreams, spawn } from 'child_process' import { Model, ModelSettingParams, PromptTemplate } from '../../../../types' import { @@ -69,7 +73,7 @@ const runModel = async (modelId: string, settingParams?: ModelSettingParams): Pr }), } - logServer(`[NITRO]::Debug: Nitro model settings: ${JSON.stringify(nitroModelSettings)}`) + log(`[SERVER]::Debug: Nitro model settings: ${JSON.stringify(nitroModelSettings)}`) // Convert settings.prompt_template to system_prompt, user_prompt, ai_prompt if (modelMetadata.settings.prompt_template) { @@ -140,7 +144,7 @@ const runNitroAndLoadModel = async (modelId: string, modelSettings: NitroModelSe } const spawnNitroProcess = async (): Promise => { - logServer(`[NITRO]::Debug: Spawning Nitro subprocess...`) + log(`[SERVER]::Debug: Spawning Nitro subprocess...`) let binaryFolder = join( getJanExtensionsPath(), @@ -155,8 +159,8 @@ const spawnNitroProcess = async (): Promise => { const args: string[] = ['1', LOCAL_HOST, NITRO_DEFAULT_PORT.toString()] // Execute the binary - logServer( - `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}` + log( + `[SERVER]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}` ) subprocess = spawn( executableOptions.executablePath, @@ -172,20 +176,20 @@ const spawnNitroProcess = async (): Promise => { // Handle subprocess output subprocess.stdout.on('data', (data: any) => { - logServer(`[NITRO]::Debug: ${data}`) + log(`[SERVER]::Debug: ${data}`) }) subprocess.stderr.on('data', (data: any) => { - logServer(`[NITRO]::Error: ${data}`) + log(`[SERVER]::Error: ${data}`) }) subprocess.on('close', (code: any) => { - logServer(`[NITRO]::Debug: Nitro exited with code: ${code}`) + log(`[SERVER]::Debug: Nitro exited with code: ${code}`) subprocess = undefined }) tcpPortUsed.waitUntilUsed(NITRO_DEFAULT_PORT, 300, 30000).then(() => { - logServer(`[NITRO]::Debug: Nitro is ready`) + log(`[SERVER]::Debug: Nitro is ready`) }) } @@ -267,7 +271,7 @@ const validateModelStatus = async (): Promise => { retries: 5, retryDelay: 500, }).then(async (res: Response) => { - logServer(`[NITRO]::Debug: Validate model state success with response ${JSON.stringify(res)}`) + log(`[SERVER]::Debug: Validate model state success with response ${JSON.stringify(res)}`) // If the response is OK, check model_loaded status. if (res.ok) { const body = await res.json() @@ -282,7 +286,7 @@ const validateModelStatus = async (): Promise => { } const loadLLMModel = async (settings: NitroModelSettings): Promise => { - logServer(`[NITRO]::Debug: Loading model with params ${JSON.stringify(settings)}`) + log(`[SERVER]::Debug: Loading model with params ${JSON.stringify(settings)}`) const fetchRT = require('fetch-retry') const fetchRetry = fetchRT(fetch) @@ -296,11 +300,11 @@ const loadLLMModel = async (settings: NitroModelSettings): Promise => retryDelay: 500, }) .then((res: any) => { - logServer(`[NITRO]::Debug: Load model success with response ${JSON.stringify(res)}`) + log(`[SERVER]::Debug: Load model success with response ${JSON.stringify(res)}`) return Promise.resolve(res) }) .catch((err: any) => { - logServer(`[NITRO]::Error: Load model failed with error ${err}`) + log(`[SERVER]::Error: Load model failed with error ${err}`) return Promise.reject(err) }) } @@ -323,7 +327,7 @@ export const stopModel = async (_modelId: string) => { }) }, 5000) const tcpPortUsed = require('tcp-port-used') - logServer(`[NITRO]::Debug: Request to kill Nitro`) + log(`[SERVER]::Debug: Request to kill Nitro`) fetch(NITRO_HTTP_KILL_URL, { method: 'DELETE', @@ -337,7 +341,7 @@ export const stopModel = async (_modelId: string) => { // don't need to do anything, we still kill the subprocess }) .then(() => tcpPortUsed.waitUntilFree(NITRO_DEFAULT_PORT, 300, 5000)) - .then(() => logServer(`[NITRO]::Debug: Nitro process is terminated`)) + .then(() => log(`[SERVER]::Debug: Nitro process is terminated`)) .then(() => resolve({ message: 'Model stopped', diff --git a/core/src/node/helper/config.ts b/core/src/node/helper/config.ts index 2b828b5769..40462abf19 100644 --- a/core/src/node/helper/config.ts +++ b/core/src/node/helper/config.ts @@ -150,31 +150,3 @@ export const getEngineConfiguration = async (engineId: string) => { full_url: undefined, } } - -/** - * Utility function to get server log path - * - * @returns {string} The log path. - */ -export const getServerLogPath = (): string => { - const appConfigurations = getAppConfigurations() - const logFolderPath = join(appConfigurations.data_folder, 'logs') - if (!fs.existsSync(logFolderPath)) { - fs.mkdirSync(logFolderPath, { recursive: true }) - } - return join(logFolderPath, 'server.log') -} - -/** - * Utility function to get app log path - * - * @returns {string} The log path. - */ -export const getAppLogPath = (): string => { - const appConfigurations = getAppConfigurations() - const logFolderPath = join(appConfigurations.data_folder, 'logs') - if (!fs.existsSync(logFolderPath)) { - fs.mkdirSync(logFolderPath, { recursive: true }) - } - return join(logFolderPath, 'app.log') -} diff --git a/core/src/node/helper/index.ts b/core/src/node/helper/index.ts index 6fc54fc6b1..51030023f8 100644 --- a/core/src/node/helper/index.ts +++ b/core/src/node/helper/index.ts @@ -1,6 +1,6 @@ export * from './config' export * from './download' -export * from './log' +export * from './logger' export * from './module' export * from './path' export * from './resource' diff --git a/core/src/node/helper/log.ts b/core/src/node/helper/log.ts deleted file mode 100644 index 8ff1969434..0000000000 --- a/core/src/node/helper/log.ts +++ /dev/null @@ -1,37 +0,0 @@ -import fs from 'fs' -import util from 'util' -import { getAppLogPath, getServerLogPath } from './config' - -export const log = (message: string) => { - const path = getAppLogPath() - if (!message.startsWith('[')) { - message = `[APP]::${message}` - } - - message = `${new Date().toISOString()} ${message}` - - writeLog(message, path) -} - -export const logServer = (message: string) => { - const path = getServerLogPath() - if (!message.startsWith('[')) { - message = `[SERVER]::${message}` - } - - message = `${new Date().toISOString()} ${message}` - writeLog(message, path) -} - -const writeLog = (message: string, logPath: string) => { - if (!fs.existsSync(logPath)) { - fs.writeFileSync(logPath, message) - } else { - const logFile = fs.createWriteStream(logPath, { - flags: 'a', - }) - logFile.write(util.format(message) + '\n') - logFile.close() - console.debug(message) - } -} diff --git a/core/src/node/helper/logger.ts b/core/src/node/helper/logger.ts new file mode 100644 index 0000000000..a6b3c8befb --- /dev/null +++ b/core/src/node/helper/logger.ts @@ -0,0 +1,81 @@ +// Abstract Logger class that all loggers should extend. +export abstract class Logger { + // Each logger must have a unique name. + abstract name: string + + /** + * Log message to log file. + * This method should be overridden by subclasses to provide specific logging behavior. + */ + abstract log(args: any): void +} + +// LoggerManager is a singleton class that manages all registered loggers. +export class LoggerManager { + // Map of registered loggers, keyed by their names. + public loggers = new Map() + + // Array to store logs that are queued before the loggers are registered. + queuedLogs: any[] = [] + + // Flag to indicate whether flushLogs is currently running. + private isFlushing = false + + // Register a new logger. If a logger with the same name already exists, it will be replaced. + register(logger: Logger) { + this.loggers.set(logger.name, logger) + } + // Unregister a logger by its name. + unregister(name: string) { + this.loggers.delete(name) + } + + get(name: string) { + return this.loggers.get(name) + } + + // Flush queued logs to all registered loggers. + flushLogs() { + // If flushLogs is already running, do nothing. + if (this.isFlushing) { + return + } + + this.isFlushing = true + + while (this.queuedLogs.length > 0 && this.loggers.size > 0) { + const log = this.queuedLogs.shift() + this.loggers.forEach((logger) => { + logger.log(log) + }) + } + + this.isFlushing = false + } + + // Log message using all registered loggers. + log(args: any) { + this.queuedLogs.push(args) + + this.flushLogs() + } + + /** + * The instance of the logger. + * If an instance doesn't exist, it creates a new one. + * This ensures that there is only one LoggerManager instance at any time. + */ + static instance(): LoggerManager { + let instance: LoggerManager | undefined = global.core?.logger + if (!instance) { + instance = new LoggerManager() + if (!global.core) global.core = {} + global.core.logger = instance + } + return instance + } +} + +export const log = (...args: any) => { + LoggerManager.instance().log(args) +} diff --git a/core/src/node/helper/resource.ts b/core/src/node/helper/resource.ts index faaaace05e..27e86c650c 100644 --- a/core/src/node/helper/resource.ts +++ b/core/src/node/helper/resource.ts @@ -1,11 +1,10 @@ import { SystemResourceInfo } from '../../types' import { physicalCpuCount } from './config' -import { log } from './log' +import { log } from './logger' export const getSystemResourceInfo = async (): Promise => { const cpu = await physicalCpuCount() - const message = `[NITRO]::CPU informations - ${cpu}` - log(message) + log(`[NITRO]::CPU informations - ${cpu}`) return { numCpuPhysicalCore: cpu, diff --git a/core/src/types/monitoring/monitoringInterface.ts b/core/src/types/monitoring/monitoringInterface.ts index ffdbebcc1b..56e785f042 100644 --- a/core/src/types/monitoring/monitoringInterface.ts +++ b/core/src/types/monitoring/monitoringInterface.ts @@ -1,3 +1,5 @@ +import { GpuSetting, OperatingSystemInfo } from '../miscellaneous' + /** * Monitoring extension for system monitoring. * @extends BaseExtension @@ -14,4 +16,14 @@ export interface MonitoringInterface { * @returns {Promise} A promise that resolves with the current system load. */ getCurrentLoad(): Promise + + /** + * Returns the GPU configuration. + */ + getGpuSetting(): Promise + + /** + * Returns information about the operating system. + */ + getOsInfo(): Promise } diff --git a/electron/main.ts b/electron/main.ts index 3331c94002..1f4719e8d4 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -24,7 +24,6 @@ import { cleanUpAndQuit } from './utils/clean' import { setupExtensions } from './utils/extension' import { setupCore } from './utils/setup' import { setupReactDevTool } from './utils/dev' -import { cleanLogs } from './utils/log' import { trayManager } from './managers/tray' import { logSystemInfo } from './utils/system' @@ -75,7 +74,6 @@ app } }) }) - .then(() => cleanLogs()) app.on('second-instance', (_event, _commandLine, _workingDirectory) => { windowManager.showMainWindow() @@ -111,7 +109,6 @@ function createMainWindow() { windowManager.createMainWindow(preloadPath, startUrl) } - /** * Handles various IPC messages from the renderer process. */ diff --git a/electron/utils/log.ts b/electron/utils/log.ts deleted file mode 100644 index 9dcd4563bb..0000000000 --- a/electron/utils/log.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { getJanDataFolderPath } from '@janhq/core/node' -import * as fs from 'fs' -import * as path from 'path' - -export function cleanLogs( - maxFileSizeBytes?: number | undefined, - daysToKeep?: number | undefined, - delayMs?: number | undefined -): void { - const size = maxFileSizeBytes ?? 1 * 1024 * 1024 // 1 MB - const days = daysToKeep ?? 7 // 7 days - const delays = delayMs ?? 10000 // 10 seconds - const logDirectory = path.join(getJanDataFolderPath(), 'logs') - - // Perform log cleaning - const currentDate = new Date() - fs.readdir(logDirectory, (err, files) => { - if (err) { - console.error('Error reading log directory:', err) - return - } - - files.forEach((file) => { - const filePath = path.join(logDirectory, file) - fs.stat(filePath, (err, stats) => { - if (err) { - console.error('Error getting file stats:', err) - return - } - - // Check size - if (stats.size > size) { - fs.unlink(filePath, (err) => { - if (err) { - console.error('Error deleting log file:', err) - return - } - console.debug( - `Deleted log file due to exceeding size limit: ${filePath}` - ) - }) - } else { - // Check age - const creationDate = new Date(stats.ctime) - const daysDifference = Math.floor( - (currentDate.getTime() - creationDate.getTime()) / - (1000 * 3600 * 24) - ) - if (daysDifference > days) { - fs.unlink(filePath, (err) => { - if (err) { - console.error('Error deleting log file:', err) - return - } - console.debug(`Deleted old log file: ${filePath}`) - }) - } - } - }) - }) - }) - - // Schedule the next execution with doubled delays - setTimeout(() => { - cleanLogs(maxFileSizeBytes, daysToKeep, delays * 2) - }, delays) -} diff --git a/extensions/assistant-extension/README.md b/extensions/assistant-extension/README.md index 16cde13924..f9690da09d 100644 --- a/extensions/assistant-extension/README.md +++ b/extensions/assistant-extension/README.md @@ -1,14 +1,10 @@ -# Jan Assistant plugin +# Create a Jan Extension using Typescript -Created using Jan app example +Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀 -# Create a Jan Plugin using Typescript +## Create Your Own Extension -Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀 - -## Create Your Own Plugin - -To create your own plugin, you can use this repository as a template! Just follow the below instructions: +To create your own extension, you can use this repository as a template! Just follow the below instructions: 1. Click the Use this template button at the top of the repository 2. Select Create a new repository @@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo ## Initial Setup -After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin. +After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension. > [!NOTE] > @@ -43,35 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne 1. :white_check_mark: Check your artifact - There will be a tgz file in your plugin directory now + There will be a tgz file in your extension directory now -## Update the Plugin Metadata +## Update the Extension Metadata -The [`package.json`](package.json) file defines metadata about your plugin, such as -plugin name, main entry, description and version. +The [`package.json`](package.json) file defines metadata about your extension, such as +extension name, main entry, description and version. -When you copy this repository, update `package.json` with the name, description for your plugin. +When you copy this repository, update `package.json` with the name, description for your extension. -## Update the Plugin Code +## Update the Extension Code -The [`src/`](./src/) directory is the heart of your plugin! This contains the -source code that will be run when your plugin extension functions are invoked. You can replace the +The [`src/`](./src/) directory is the heart of your extension! This contains the +source code that will be run when your extension functions are invoked. You can replace the contents of this directory with your own code. -There are a few things to keep in mind when writing your plugin code: +There are a few things to keep in mind when writing your extension code: -- Most Jan Plugin Extension functions are processed asynchronously. +- Most Jan Extension functions are processed asynchronously. In `index.ts`, you will see that the extension function will return a `Promise`. ```typescript - import { core } from "@janhq/core"; + import { events, MessageEvent, MessageRequest } from '@janhq/core' function onStart(): Promise { - return core.invokePluginFunc(MODULE_PATH, "run", 0); + return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + this.inference(data) + ) } ``` - For more information about the Jan Plugin Core module, see the + For more information about the Jan Extension Core module, see the [documentation](https://github.com/janhq/jan/blob/main/core/README.md). -So, what are you waiting for? Go ahead and start customizing your plugin! +So, what are you waiting for? Go ahead and start customizing your extension! diff --git a/extensions/huggingface-extension/README.md b/extensions/huggingface-extension/README.md index ae70eb4ecd..f9690da09d 100644 --- a/extensions/huggingface-extension/README.md +++ b/extensions/huggingface-extension/README.md @@ -1,10 +1,10 @@ -# Create a Jan Plugin using Typescript +# Create a Jan Extension using Typescript -Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀 +Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀 -## Create Your Own Plugin +## Create Your Own Extension -To create your own plugin, you can use this repository as a template! Just follow the below instructions: +To create your own extension, you can use this repository as a template! Just follow the below instructions: 1. Click the Use this template button at the top of the repository 2. Select Create a new repository @@ -14,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo ## Initial Setup -After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin. +After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension. > [!NOTE] > @@ -39,35 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne 1. :white_check_mark: Check your artifact - There will be a tgz file in your plugin directory now + There will be a tgz file in your extension directory now -## Update the Plugin Metadata +## Update the Extension Metadata -The [`package.json`](package.json) file defines metadata about your plugin, such as -plugin name, main entry, description and version. +The [`package.json`](package.json) file defines metadata about your extension, such as +extension name, main entry, description and version. -When you copy this repository, update `package.json` with the name, description for your plugin. +When you copy this repository, update `package.json` with the name, description for your extension. -## Update the Plugin Code +## Update the Extension Code -The [`src/`](./src/) directory is the heart of your plugin! This contains the -source code that will be run when your plugin extension functions are invoked. You can replace the +The [`src/`](./src/) directory is the heart of your extension! This contains the +source code that will be run when your extension functions are invoked. You can replace the contents of this directory with your own code. -There are a few things to keep in mind when writing your plugin code: +There are a few things to keep in mind when writing your extension code: -- Most Jan Plugin Extension functions are processed asynchronously. +- Most Jan Extension functions are processed asynchronously. In `index.ts`, you will see that the extension function will return a `Promise`. ```typescript - import { core } from "@janhq/core"; + import { events, MessageEvent, MessageRequest } from '@janhq/core' function onStart(): Promise { - return core.invokePluginFunc(MODULE_PATH, "run", 0); + return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + this.inference(data) + ) } ``` - For more information about the Jan Plugin Core module, see the + For more information about the Jan Extension Core module, see the [documentation](https://github.com/janhq/jan/blob/main/core/README.md). -So, what are you waiting for? Go ahead and start customizing your plugin! +So, what are you waiting for? Go ahead and start customizing your extension! diff --git a/extensions/inference-groq-extension/README.md b/extensions/inference-groq-extension/README.md index 455783efb1..f9690da09d 100644 --- a/extensions/inference-groq-extension/README.md +++ b/extensions/inference-groq-extension/README.md @@ -1,14 +1,10 @@ -# Jan inference plugin +# Create a Jan Extension using Typescript -Created using Jan app example +Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀 -# Create a Jan Plugin using Typescript +## Create Your Own Extension -Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀 - -## Create Your Own Plugin - -To create your own plugin, you can use this repository as a template! Just follow the below instructions: +To create your own extension, you can use this repository as a template! Just follow the below instructions: 1. Click the Use this template button at the top of the repository 2. Select Create a new repository @@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo ## Initial Setup -After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin. +After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension. > [!NOTE] > @@ -43,36 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne 1. :white_check_mark: Check your artifact - There will be a tgz file in your plugin directory now + There will be a tgz file in your extension directory now -## Update the Plugin Metadata +## Update the Extension Metadata -The [`package.json`](package.json) file defines metadata about your plugin, such as -plugin name, main entry, description and version. +The [`package.json`](package.json) file defines metadata about your extension, such as +extension name, main entry, description and version. -When you copy this repository, update `package.json` with the name, description for your plugin. +When you copy this repository, update `package.json` with the name, description for your extension. -## Update the Plugin Code +## Update the Extension Code -The [`src/`](./src/) directory is the heart of your plugin! This contains the -source code that will be run when your plugin extension functions are invoked. You can replace the +The [`src/`](./src/) directory is the heart of your extension! This contains the +source code that will be run when your extension functions are invoked. You can replace the contents of this directory with your own code. -There are a few things to keep in mind when writing your plugin code: +There are a few things to keep in mind when writing your extension code: -- Most Jan Plugin Extension functions are processed asynchronously. +- Most Jan Extension functions are processed asynchronously. In `index.ts`, you will see that the extension function will return a `Promise`. ```typescript - import { core } from "@janhq/core"; + import { events, MessageEvent, MessageRequest } from '@janhq/core' function onStart(): Promise { - return core.invokePluginFunc(MODULE_PATH, "run", 0); + return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + this.inference(data) + ) } ``` - For more information about the Jan Plugin Core module, see the + For more information about the Jan Extension Core module, see the [documentation](https://github.com/janhq/jan/blob/main/core/README.md). -So, what are you waiting for? Go ahead and start customizing your plugin! - +So, what are you waiting for? Go ahead and start customizing your extension! diff --git a/extensions/inference-nitro-extension/README.md b/extensions/inference-nitro-extension/README.md index f499e0b9c5..f9690da09d 100644 --- a/extensions/inference-nitro-extension/README.md +++ b/extensions/inference-nitro-extension/README.md @@ -1,14 +1,10 @@ -# Jan inference plugin +# Create a Jan Extension using Typescript -Created using Jan app example +Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀 -# Create a Jan Plugin using Typescript +## Create Your Own Extension -Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀 - -## Create Your Own Plugin - -To create your own plugin, you can use this repository as a template! Just follow the below instructions: +To create your own extension, you can use this repository as a template! Just follow the below instructions: 1. Click the Use this template button at the top of the repository 2. Select Create a new repository @@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo ## Initial Setup -After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin. +After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension. > [!NOTE] > @@ -43,35 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne 1. :white_check_mark: Check your artifact - There will be a tgz file in your plugin directory now + There will be a tgz file in your extension directory now -## Update the Plugin Metadata +## Update the Extension Metadata -The [`package.json`](package.json) file defines metadata about your plugin, such as -plugin name, main entry, description and version. +The [`package.json`](package.json) file defines metadata about your extension, such as +extension name, main entry, description and version. -When you copy this repository, update `package.json` with the name, description for your plugin. +When you copy this repository, update `package.json` with the name, description for your extension. -## Update the Plugin Code +## Update the Extension Code -The [`src/`](./src/) directory is the heart of your plugin! This contains the -source code that will be run when your plugin extension functions are invoked. You can replace the +The [`src/`](./src/) directory is the heart of your extension! This contains the +source code that will be run when your extension functions are invoked. You can replace the contents of this directory with your own code. -There are a few things to keep in mind when writing your plugin code: +There are a few things to keep in mind when writing your extension code: -- Most Jan Plugin Extension functions are processed asynchronously. +- Most Jan Extension functions are processed asynchronously. In `index.ts`, you will see that the extension function will return a `Promise`. ```typescript - import { core } from '@janhq/core' + import { events, MessageEvent, MessageRequest } from '@janhq/core' function onStart(): Promise { - return core.invokePluginFunc(MODULE_PATH, 'run', 0) + return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + this.inference(data) + ) } ``` - For more information about the Jan Plugin Core module, see the + For more information about the Jan Extension Core module, see the [documentation](https://github.com/janhq/jan/blob/main/core/README.md). -So, what are you waiting for? Go ahead and start customizing your plugin! +So, what are you waiting for? Go ahead and start customizing your extension! diff --git a/extensions/inference-triton-trtllm-extension/README.md b/extensions/inference-triton-trtllm-extension/README.md index 455783efb1..f9690da09d 100644 --- a/extensions/inference-triton-trtllm-extension/README.md +++ b/extensions/inference-triton-trtllm-extension/README.md @@ -1,14 +1,10 @@ -# Jan inference plugin +# Create a Jan Extension using Typescript -Created using Jan app example +Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀 -# Create a Jan Plugin using Typescript +## Create Your Own Extension -Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀 - -## Create Your Own Plugin - -To create your own plugin, you can use this repository as a template! Just follow the below instructions: +To create your own extension, you can use this repository as a template! Just follow the below instructions: 1. Click the Use this template button at the top of the repository 2. Select Create a new repository @@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo ## Initial Setup -After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin. +After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension. > [!NOTE] > @@ -43,36 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne 1. :white_check_mark: Check your artifact - There will be a tgz file in your plugin directory now + There will be a tgz file in your extension directory now -## Update the Plugin Metadata +## Update the Extension Metadata -The [`package.json`](package.json) file defines metadata about your plugin, such as -plugin name, main entry, description and version. +The [`package.json`](package.json) file defines metadata about your extension, such as +extension name, main entry, description and version. -When you copy this repository, update `package.json` with the name, description for your plugin. +When you copy this repository, update `package.json` with the name, description for your extension. -## Update the Plugin Code +## Update the Extension Code -The [`src/`](./src/) directory is the heart of your plugin! This contains the -source code that will be run when your plugin extension functions are invoked. You can replace the +The [`src/`](./src/) directory is the heart of your extension! This contains the +source code that will be run when your extension functions are invoked. You can replace the contents of this directory with your own code. -There are a few things to keep in mind when writing your plugin code: +There are a few things to keep in mind when writing your extension code: -- Most Jan Plugin Extension functions are processed asynchronously. +- Most Jan Extension functions are processed asynchronously. In `index.ts`, you will see that the extension function will return a `Promise`. ```typescript - import { core } from "@janhq/core"; + import { events, MessageEvent, MessageRequest } from '@janhq/core' function onStart(): Promise { - return core.invokePluginFunc(MODULE_PATH, "run", 0); + return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + this.inference(data) + ) } ``` - For more information about the Jan Plugin Core module, see the + For more information about the Jan Extension Core module, see the [documentation](https://github.com/janhq/jan/blob/main/core/README.md). -So, what are you waiting for? Go ahead and start customizing your plugin! - +So, what are you waiting for? Go ahead and start customizing your extension! diff --git a/extensions/model-extension/README.md b/extensions/model-extension/README.md index 516bbec8b4..f9690da09d 100644 --- a/extensions/model-extension/README.md +++ b/extensions/model-extension/README.md @@ -1,14 +1,10 @@ -# Jan Model Management plugin +# Create a Jan Extension using Typescript -Created using Jan app example +Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀 -# Create a Jan Plugin using Typescript +## Create Your Own Extension -Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀 - -## Create Your Own Plugin - -To create your own plugin, you can use this repository as a template! Just follow the below instructions: +To create your own extension, you can use this repository as a template! Just follow the below instructions: 1. Click the Use this template button at the top of the repository 2. Select Create a new repository @@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo ## Initial Setup -After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin. +After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension. > [!NOTE] > @@ -43,36 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne 1. :white_check_mark: Check your artifact - There will be a tgz file in your plugin directory now + There will be a tgz file in your extension directory now -## Update the Plugin Metadata +## Update the Extension Metadata -The [`package.json`](package.json) file defines metadata about your plugin, such as -plugin name, main entry, description and version. +The [`package.json`](package.json) file defines metadata about your extension, such as +extension name, main entry, description and version. -When you copy this repository, update `package.json` with the name, description for your plugin. +When you copy this repository, update `package.json` with the name, description for your extension. -## Update the Plugin Code +## Update the Extension Code -The [`src/`](./src/) directory is the heart of your plugin! This contains the -source code that will be run when your plugin extension functions are invoked. You can replace the +The [`src/`](./src/) directory is the heart of your extension! This contains the +source code that will be run when your extension functions are invoked. You can replace the contents of this directory with your own code. -There are a few things to keep in mind when writing your plugin code: +There are a few things to keep in mind when writing your extension code: -- Most Jan Plugin Extension functions are processed asynchronously. +- Most Jan Extension functions are processed asynchronously. In `index.ts`, you will see that the extension function will return a `Promise`. ```typescript - import { core } from "@janhq/core"; + import { events, MessageEvent, MessageRequest } from '@janhq/core' function onStart(): Promise { - return core.invokePluginFunc(MODULE_PATH, "run", 0); + return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + this.inference(data) + ) } ``` - For more information about the Jan Plugin Core module, see the + For more information about the Jan Extension Core module, see the [documentation](https://github.com/janhq/jan/blob/main/core/README.md). -So, what are you waiting for? Go ahead and start customizing your plugin! - +So, what are you waiting for? Go ahead and start customizing your extension! diff --git a/extensions/monitoring-extension/README.md b/extensions/monitoring-extension/README.md index 1617b9b131..f9690da09d 100644 --- a/extensions/monitoring-extension/README.md +++ b/extensions/monitoring-extension/README.md @@ -1,14 +1,10 @@ -# Jan Monitoring plugin +# Create a Jan Extension using Typescript -Created using Jan app example +Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀 -# Create a Jan Plugin using Typescript +## Create Your Own Extension -Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀 - -## Create Your Own Plugin - -To create your own plugin, you can use this repository as a template! Just follow the below instructions: +To create your own extension, you can use this repository as a template! Just follow the below instructions: 1. Click the Use this template button at the top of the repository 2. Select Create a new repository @@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo ## Initial Setup -After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin. +After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension. > [!NOTE] > @@ -43,36 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne 1. :white_check_mark: Check your artifact - There will be a tgz file in your plugin directory now + There will be a tgz file in your extension directory now -## Update the Plugin Metadata +## Update the Extension Metadata -The [`package.json`](package.json) file defines metadata about your plugin, such as -plugin name, main entry, description and version. +The [`package.json`](package.json) file defines metadata about your extension, such as +extension name, main entry, description and version. -When you copy this repository, update `package.json` with the name, description for your plugin. +When you copy this repository, update `package.json` with the name, description for your extension. -## Update the Plugin Code +## Update the Extension Code -The [`src/`](./src/) directory is the heart of your plugin! This contains the -source code that will be run when your plugin extension functions are invoked. You can replace the +The [`src/`](./src/) directory is the heart of your extension! This contains the +source code that will be run when your extension functions are invoked. You can replace the contents of this directory with your own code. -There are a few things to keep in mind when writing your plugin code: +There are a few things to keep in mind when writing your extension code: -- Most Jan Plugin Extension functions are processed asynchronously. +- Most Jan Extension functions are processed asynchronously. In `index.ts`, you will see that the extension function will return a `Promise`. ```typescript - import { core } from "@janhq/core"; + import { events, MessageEvent, MessageRequest } from '@janhq/core' function onStart(): Promise { - return core.invokePluginFunc(MODULE_PATH, "run", 0); + return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + this.inference(data) + ) } ``` - For more information about the Jan Plugin Core module, see the + For more information about the Jan Extension Core module, see the [documentation](https://github.com/janhq/jan/blob/main/core/README.md). -So, what are you waiting for? Go ahead and start customizing your plugin! - +So, what are you waiting for? Go ahead and start customizing your extension! diff --git a/extensions/monitoring-extension/resources/settings.json b/extensions/monitoring-extension/resources/settings.json new file mode 100644 index 0000000000..fbdaf309a9 --- /dev/null +++ b/extensions/monitoring-extension/resources/settings.json @@ -0,0 +1,21 @@ +[ + { + "key": "log-enabled", + "title": "App Logging Enabled", + "description": "We recommend enabling this setting to help us improve the app. Your data will be kept private on your computer, and you can opt out at any time.", + "controllerType": "checkbox", + "controllerProps": { + "value": true + } + }, + { + "key": "log-cleaning-interval", + "title": "Log Cleaning Interval", + "description": "Log cleaning interval in milliseconds.", + "controllerType": "input", + "controllerProps": { + "value": "120000", + "placeholder": "Interval in milliseconds. E.g. 120000" + } + } +] diff --git a/extensions/monitoring-extension/rollup.config.ts b/extensions/monitoring-extension/rollup.config.ts index c5e649af40..b054d62916 100644 --- a/extensions/monitoring-extension/rollup.config.ts +++ b/extensions/monitoring-extension/rollup.config.ts @@ -4,6 +4,7 @@ import sourceMaps from 'rollup-plugin-sourcemaps' import typescript from 'rollup-plugin-typescript2' import json from '@rollup/plugin-json' import replace from '@rollup/plugin-replace' +const settingJson = require('./resources/settings.json') const packageJson = require('./package.json') export default [ @@ -19,6 +20,7 @@ export default [ replace({ preventAssignment: true, NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`), + SETTINGS: JSON.stringify(settingJson), }), // Allow json resolution json(), diff --git a/extensions/monitoring-extension/src/index.ts b/extensions/monitoring-extension/src/index.ts index 7ef40e7bec..569feafeb6 100644 --- a/extensions/monitoring-extension/src/index.ts +++ b/extensions/monitoring-extension/src/index.ts @@ -1,27 +1,63 @@ import { GpuSetting, MonitoringExtension, + MonitoringInterface, OperatingSystemInfo, executeOnMain, } from '@janhq/core' +declare const SETTINGS: Array + +enum Settings { + logEnabled = 'log-enabled', + logCleaningInterval = 'log-cleaning-interval', +} /** * JanMonitoringExtension is a extension that provides system monitoring functionality. * It implements the MonitoringExtension interface from the @janhq/core package. */ -export default class JanMonitoringExtension extends MonitoringExtension { +export default class JanMonitoringExtension + extends MonitoringExtension + implements MonitoringInterface +{ /** * Called when the extension is loaded. */ async onLoad() { + // Register extension settings + this.registerSettings(SETTINGS) + + const logEnabled = await this.getSetting(Settings.logEnabled, true) + const logCleaningInterval = parseInt( + await this.getSetting(Settings.logCleaningInterval, '120000') + ) + // Register File Logger provided by this extension + await executeOnMain(NODE, 'registerLogger', { + logEnabled, + logCleaningInterval: isNaN(logCleaningInterval) + ? 120000 + : logCleaningInterval, + }) + // Attempt to fetch nvidia info await executeOnMain(NODE, 'updateNvidiaInfo') } + onSettingUpdate(key: string, value: T): void { + if (key === Settings.logEnabled) { + executeOnMain(NODE, 'updateLogger', { logEnabled: value }) + } else if (key === Settings.logCleaningInterval) { + executeOnMain(NODE, 'updateLogger', { logCleaningInterval: value }) + } + } + /** * Called when the extension is unloaded. */ - onUnload(): void {} + onUnload(): void { + // Register File Logger provided by this extension + executeOnMain(NODE, 'unregisterLogger') + } /** * Returns the GPU configuration. @@ -47,6 +83,10 @@ export default class JanMonitoringExtension extends MonitoringExtension { return executeOnMain(NODE, 'getCurrentLoad') } + /** + * Returns information about the OS + * @returns + */ getOsInfo(): Promise { return executeOnMain(NODE, 'getOsInfo') } diff --git a/extensions/monitoring-extension/src/node/index.ts b/extensions/monitoring-extension/src/node/index.ts index dc3ee8d815..26a21ad490 100644 --- a/extensions/monitoring-extension/src/node/index.ts +++ b/extensions/monitoring-extension/src/node/index.ts @@ -1,6 +1,7 @@ import { GpuSetting, GpuSettingInfo, + LoggerManager, OperatingSystemInfo, ResourceInfo, SupportedPlatforms, @@ -12,6 +13,7 @@ import { exec } from 'child_process' import { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs' import path from 'path' import os from 'os' +import { FileLogger } from './logger' /** * Path to the settings directory @@ -346,3 +348,22 @@ export const getOsInfo = (): OperatingSystemInfo => { return osInfo } + +export const registerLogger = ({ logEnabled, logCleaningInterval }) => { + const logger = new FileLogger(logEnabled, logCleaningInterval) + LoggerManager.instance().register(logger) + logger.cleanLogs() +} + +export const unregisterLogger = () => { + LoggerManager.instance().unregister('file') +} + +export const updateLogger = ({ logEnabled, logCleaningInterval }) => { + const logger = LoggerManager.instance().loggers.get('file') as FileLogger + if (logger && logEnabled !== undefined) logger.logEnabled = logEnabled + if (logger && logCleaningInterval) + logger.logCleaningInterval = logCleaningInterval + // Rerun + logger && logger.cleanLogs() +} diff --git a/extensions/monitoring-extension/src/node/logger.ts b/extensions/monitoring-extension/src/node/logger.ts new file mode 100644 index 0000000000..3d53e5ed93 --- /dev/null +++ b/extensions/monitoring-extension/src/node/logger.ts @@ -0,0 +1,138 @@ +import fs from 'fs' +import util from 'util' +import { + getAppConfigurations, + getJanDataFolderPath, + Logger, +} from '@janhq/core/node' +import path, { join } from 'path' + +export class FileLogger extends Logger { + name = 'file' + logCleaningInterval: number = 120000 + timeout: NodeJS.Timeout | null = null + appLogPath: string = './' + logEnabled: boolean = true + + constructor( + logEnabled: boolean = true, + logCleaningInterval: number = 120000 + ) { + super() + this.logEnabled = logEnabled + if (logCleaningInterval) this.logCleaningInterval = logCleaningInterval + + const appConfigurations = getAppConfigurations() + const logFolderPath = join(appConfigurations.data_folder, 'logs') + if (!fs.existsSync(logFolderPath)) { + fs.mkdirSync(logFolderPath, { recursive: true }) + } + + this.appLogPath = join(logFolderPath, 'app.log') + } + + log(args: any) { + if (!this.logEnabled) return + let message = args[0] + const scope = args[1] + if (!message) return + const path = this.appLogPath + if (!scope && !message.startsWith('[')) { + message = `[APP]::${message}` + } else if (scope) { + message = `${scope}::${message}` + } + + message = `${new Date().toISOString()} ${message}` + + writeLog(message, path) + } + + cleanLogs( + maxFileSizeBytes?: number | undefined, + daysToKeep?: number | undefined + ): void { + // clear existing timeout + // incase we rerun it with different values + if (this.timeout) clearTimeout(this.timeout) + this.timeout = undefined + + if (!this.logEnabled) return + + console.log( + 'Validating app logs. Next attempt in ', + this.logCleaningInterval + ) + + const size = maxFileSizeBytes ?? 1 * 1024 * 1024 // 1 MB + const days = daysToKeep ?? 7 // 7 days + const logDirectory = path.join(getJanDataFolderPath(), 'logs') + + // Perform log cleaning + const currentDate = new Date() + fs.readdir(logDirectory, (err, files) => { + if (err) { + console.error('Error reading log directory:', err) + return + } + + files.forEach((file) => { + const filePath = path.join(logDirectory, file) + fs.stat(filePath, (err, stats) => { + if (err) { + console.error('Error getting file stats:', err) + return + } + + // Check size + if (stats.size > size) { + fs.unlink(filePath, (err) => { + if (err) { + console.error('Error deleting log file:', err) + return + } + console.debug( + `Deleted log file due to exceeding size limit: ${filePath}` + ) + }) + } else { + // Check age + const creationDate = new Date(stats.ctime) + const daysDifference = Math.floor( + (currentDate.getTime() - creationDate.getTime()) / + (1000 * 3600 * 24) + ) + if (daysDifference > days) { + fs.unlink(filePath, (err) => { + if (err) { + console.error('Error deleting log file:', err) + return + } + console.debug(`Deleted old log file: ${filePath}`) + }) + } + } + }) + }) + }) + + // Schedule the next execution with doubled delays + this.timeout = setTimeout( + () => this.cleanLogs(maxFileSizeBytes, daysToKeep), + this.logCleaningInterval + ) + } +} + +const writeLog = (message: string, logPath: string) => { + if (!fs.existsSync(logPath)) { + fs.writeFileSync(logPath, message) + } else { + const logFile = fs.createWriteStream(logPath, { + flags: 'a', + }) + logFile.write(util.format(message) + '\n') + logFile.close() + console.debug(message) + } +} diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts new file mode 100644 index 0000000000..c8d4af4281 --- /dev/null +++ b/server/helpers/logger.ts @@ -0,0 +1,35 @@ +import { log } from '@janhq/core/node' +import { FastifyBaseLogger } from 'fastify' +import { ChildLoggerOptions } from 'fastify/types/logger' +import pino from 'pino' + +export class Logger implements FastifyBaseLogger { + child( + bindings: pino.Bindings, + options?: ChildLoggerOptions | undefined + ): FastifyBaseLogger { + return new Logger() + } + level = 'info' + + silent = () => {} + + info = function (msg: any) { + log(msg) + } + error = function (msg: any) { + log(msg) + } + debug = function (msg: any) { + log(msg) + } + fatal = function (msg: any) { + log(msg) + } + warn = function (msg: any) { + log(msg) + } + trace = function (msg: any) { + log(msg) + } +} diff --git a/server/index.ts b/server/index.ts index 1cd9eaa58e..f82c4f5bc6 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,13 +1,9 @@ import fastify from 'fastify' import dotenv from 'dotenv' -import { - getServerLogPath, - v1Router, - logServer, - getJanExtensionsPath, -} from '@janhq/core/node' +import { v1Router, log, getJanExtensionsPath } from '@janhq/core/node' import { join } from 'path' import tcpPortUsed from 'tcp-port-used' +import { Logger } from './helpers/logger' // Load environment variables dotenv.config() @@ -52,7 +48,7 @@ export const startServer = async (configs?: ServerConfig): Promise => { const inUse = await tcpPortUsed.check(Number(configs.port), configs.host) if (inUse) { const errorMessage = `Port ${configs.port} is already in use.` - logServer(errorMessage) + log(errorMessage, '[SERVER]') throw new Error(errorMessage) } } @@ -62,19 +58,15 @@ export const startServer = async (configs?: ServerConfig): Promise => { hostSetting = configs?.host ?? JAN_API_HOST portSetting = configs?.port ?? JAN_API_PORT corsEnabled = configs?.isCorsEnabled ?? true - const serverLogPath = getServerLogPath() // Start the server try { // Log server start - if (isVerbose) logServer(`Debug: Starting JAN API server...`) + if (isVerbose) log(`Debug: Starting JAN API server...`, '[SERVER]') // Initialize Fastify server with logging server = fastify({ - logger: { - level: 'info', - file: serverLogPath, - }, + logger: new Logger(), }) // Register CORS if enabled @@ -134,14 +126,15 @@ export const startServer = async (configs?: ServerConfig): Promise => { .then(() => { // Log server listening if (isVerbose) - logServer( - `Debug: JAN API listening at: http://${hostSetting}:${portSetting}` + log( + `Debug: JAN API listening at: http://${hostSetting}:${portSetting}`, + '[SERVER]' ) }) return true } catch (e) { // Log any errors - if (isVerbose) logServer(`Error: ${e}`) + if (isVerbose) log(`Error: ${e}`, '[SERVER]') } return false } @@ -152,11 +145,11 @@ export const startServer = async (configs?: ServerConfig): Promise => { export const stopServer = async () => { try { // Log server stop - if (isVerbose) logServer(`Debug: Server stopped`) + if (isVerbose) log(`Debug: Server stopped`, '[SERVER]') // Stop the server await server?.close() } catch (e) { // Log any errors - if (isVerbose) logServer(`Error: ${e}`) + if (isVerbose) log(`Error: ${e}`, '[SERVER]') } } diff --git a/web/containers/ServerLogs/index.tsx b/web/containers/ServerLogs/index.tsx index 36e22bc1ed..f423a08733 100644 --- a/web/containers/ServerLogs/index.tsx +++ b/web/containers/ServerLogs/index.tsx @@ -28,9 +28,11 @@ const ServerLogs = (props: ServerLogsProps) => { const updateLogs = useCallback( () => - getLogs('server').then((log) => { + getLogs('app').then((log) => { if (typeof log?.split === 'function') { - setLogs(log.split(/\r?\n|\r|\n/g)) + setLogs( + log.split(/\r?\n|\r|\n/g).filter((e) => e.includes('[SERVER]::')) + ) } }), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/extension/ExtensionManager.ts b/web/extension/ExtensionManager.ts index 9bda2033ee..42bca79bf9 100644 --- a/web/extension/ExtensionManager.ts +++ b/web/extension/ExtensionManager.ts @@ -19,7 +19,8 @@ export class ExtensionManager { * @param extension - The extension to register. */ register(name: string, extension: T) { - this.extensions.set(extension.type() ?? name, extension) + // Register for naming use + this.extensions.set(name, extension) // Register AI Engines if ('provider' in extension && typeof extension.provider === 'string') { @@ -35,10 +36,26 @@ export class ExtensionManager { * @param type - The type of the extension to retrieve. * @returns The extension, if found. */ - get( - type: ExtensionTypeEnum | string - ): T | undefined { - return this.extensions.get(type) as T | undefined + get(type: ExtensionTypeEnum): T | undefined { + return this.getAll().findLast((e) => e.type() === type) as T | undefined + } + + /** + * Retrieves a extension by its type. + * @param type - The type of the extension to retrieve. + * @returns The extension, if found. + */ + getByName(name: string): BaseExtension | undefined { + return this.extensions.get(name) as BaseExtension | undefined + } + + /** + * Retrieves a extension by its type. + * @param type - The type of the extension to retrieve. + * @returns The extension, if found. + */ + getAll(): BaseExtension[] { + return Array.from(this.extensions.values()) } /** diff --git a/web/hooks/useLogs.tsx b/web/hooks/useLogs.tsx index 73733fbb87..a391a22782 100644 --- a/web/hooks/useLogs.tsx +++ b/web/hooks/useLogs.tsx @@ -25,12 +25,12 @@ export const useLogs = () => { ) const openServerLog = useCallback(async () => { - const fullPath = await joinPath([janDataFolderPath, 'logs', 'server.log']) + const fullPath = await joinPath([janDataFolderPath, 'logs', 'app.log']) return openFileExplorer(fullPath) }, [janDataFolderPath]) const clearServerLog = useCallback(async () => { - await fs.writeFileSync(await joinPath(['file://logs', 'server.log']), '') + await fs.writeFileSync(await joinPath(['file://logs', 'app.log']), '') }, []) return { getLogs, openServerLog, clearServerLog } diff --git a/web/screens/Settings/CoreExtensions/TensorRtExtensionItem.tsx b/web/screens/Settings/CoreExtensions/TensorRtExtensionItem.tsx index fb0214536a..161ec8bd21 100644 --- a/web/screens/Settings/CoreExtensions/TensorRtExtensionItem.tsx +++ b/web/screens/Settings/CoreExtensions/TensorRtExtensionItem.tsx @@ -79,7 +79,7 @@ const TensorRtExtensionItem: React.FC = ({ item }) => { useEffect(() => { const getExtensionInstallationState = async () => { - const extension = extensionManager.get(item.name ?? '') + const extension = extensionManager.getByName(item.name ?? '') if (!extension) return if (typeof extension?.installationState === 'function') { @@ -92,13 +92,13 @@ const TensorRtExtensionItem: React.FC = ({ item }) => { }, [item.name, isInstalling]) useEffect(() => { - const extension = extensionManager.get(item.name ?? '') + const extension = extensionManager.getByName(item.name ?? '') if (!extension) return setCompatibility(extension.compatibility()) }, [setCompatibility, item.name]) const onInstallClick = useCallback(async () => { - const extension = extensionManager.get(item.name ?? '') + const extension = extensionManager.getByName(item.name ?? '') if (!extension) return await extension.install() diff --git a/web/screens/Settings/ExtensionSetting/index.tsx b/web/screens/Settings/ExtensionSetting/index.tsx index d5563b835d..41b82230dc 100644 --- a/web/screens/Settings/ExtensionSetting/index.tsx +++ b/web/screens/Settings/ExtensionSetting/index.tsx @@ -17,13 +17,12 @@ const ExtensionSetting: React.FC = () => { const getExtensionSettings = async () => { if (!selectedExtensionName) return const allSettings: SettingComponentProps[] = [] - const baseExtension = extensionManager.get(selectedExtensionName) + const baseExtension = extensionManager.getByName(selectedExtensionName) if (!baseExtension) return if (typeof baseExtension.getSettings === 'function') { const setting = await baseExtension.getSettings() if (setting) allSettings.push(...setting) } - setSettings(allSettings) } getExtensionSettings() @@ -40,7 +39,7 @@ const ExtensionSetting: React.FC = () => { const extensionName = setting.extensionName if (extensionName) { - extensionManager.get(extensionName)?.updateSettings([setting]) + extensionManager.getByName(extensionName)?.updateSettings([setting]) } return setting diff --git a/web/screens/Settings/SettingDetail/SettingDetailItem/SettingDetailToggleItem/index.tsx b/web/screens/Settings/SettingDetail/SettingDetailItem/SettingDetailToggleItem/index.tsx new file mode 100644 index 0000000000..64b913b218 --- /dev/null +++ b/web/screens/Settings/SettingDetail/SettingDetailItem/SettingDetailToggleItem/index.tsx @@ -0,0 +1,47 @@ +import { CheckboxComponentProps, SettingComponentProps } from '@janhq/core' +import { Switch } from '@janhq/uikit' +import { Marked, Renderer } from 'marked' + +type Props = { + settingProps: SettingComponentProps + onValueChanged?: (e: boolean) => void +} + +const marked: Marked = new Marked({ + renderer: { + link: (href, title, text) => { + return Renderer.prototype.link + ?.apply(this, [href, title, text]) + .replace(' = ({ + settingProps, + onValueChanged, +}) => { + const { value } = settingProps.controllerProps as CheckboxComponentProps + + const description = marked.parse(settingProps.description ?? '', { + async: false, + }) + + return ( +
+
+

{settingProps.title}

+ { +
+ } +
+ +
+ ) +} + +export default SettingDetailToggleItem diff --git a/web/screens/Settings/SettingDetail/SettingDetailItem/index.tsx b/web/screens/Settings/SettingDetail/SettingDetailItem/index.tsx index 5be1e9fea3..03ac752268 100644 --- a/web/screens/Settings/SettingDetail/SettingDetailItem/index.tsx +++ b/web/screens/Settings/SettingDetail/SettingDetailItem/index.tsx @@ -1,6 +1,7 @@ import { SettingComponentProps } from '@janhq/core' import SettingDetailTextInputItem from './SettingDetailTextInputItem' +import SettingDetailToggleItem from './SettingDetailToggleItem' type Props = { componentProps: SettingComponentProps[] @@ -23,6 +24,16 @@ const SettingDetailItem: React.FC = ({ ) } + case 'checkbox': { + return ( + onValueUpdated(data.key, value)} + /> + ) + } + default: return null } diff --git a/web/screens/Settings/SettingMenu/index.tsx b/web/screens/Settings/SettingMenu/index.tsx index b558fe87b0..330e4c3c8d 100644 --- a/web/screens/Settings/SettingMenu/index.tsx +++ b/web/screens/Settings/SettingMenu/index.tsx @@ -15,20 +15,13 @@ const SettingMenu: React.FC = () => { useEffect(() => { const getAllSettings = async () => { - const activeExtensions = await extensionManager.getActive() const extensionsMenu: string[] = [] - - for (const extension of activeExtensions) { - const extensionName = extension.name - if (!extensionName) continue - - const baseExtension = extensionManager.get(extensionName) - if (!baseExtension) continue - - if (typeof baseExtension.getSettings === 'function') { - const settings = await baseExtension.getSettings() + const extensions = extensionManager.getAll() + for (const extension of extensions) { + if (typeof extension.getSettings === 'function') { + const settings = await extension.getSettings() if (settings && settings.length > 0) { - extensionsMenu.push(extensionName) + extensionsMenu.push(extension.name ?? extension.url) } } }