From d3cc345cded05d939c17667ffa44e54d73c06546 Mon Sep 17 00:00:00 2001 From: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> Date: Mon, 6 Mar 2023 10:47:04 +0200 Subject: [PATCH] Renderer file logging through IPC Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> --- packages/core/src/common/logger.injectable.ts | 11 +- .../common/logger/ipc-file-logger-channel.ts | 20 ++ .../src/common/winston-logger.injectable.ts | 18 ++ .../close-ipc-logging-listener.injectable.ts | 20 ++ .../main/logger/ipc-file-logger.injectable.ts | 24 +++ .../src/main/logger/ipc-file-logger.test.ts | 179 ++++++++++++++++++ .../core/src/main/logger/ipc-file-logger.ts | 51 +++++ .../logger/ipc-logging-listener.injectable.ts | 38 ++++ .../main/logger/ipc-logging-listener.test.ts | 31 +++ .../init-cluster-frame.injectable.ts | 2 + .../init-cluster-frame/init-cluster-frame.ts | 11 +- .../root-frame/init-root-frame.injectable.ts | 3 + .../logger/close-renderer-log-file-id.test.ts | 49 +++++ .../close-renderer-log-file.injectable.ts | 28 +++ .../logger/ipc-transport.injectable.ts | 59 ++++++ .../src/renderer/logger/ipc-transport.test.ts | 42 ++++ .../core/src/renderer/logger/ipc-transport.ts | 35 ++++ .../logger/renderer-log-file-id.injectable.ts | 29 +++ .../logger/renderer-log-file-id.test.ts | 27 +++ 19 files changed, 666 insertions(+), 11 deletions(-) create mode 100644 packages/core/src/common/logger/ipc-file-logger-channel.ts create mode 100644 packages/core/src/common/winston-logger.injectable.ts create mode 100644 packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts create mode 100644 packages/core/src/main/logger/ipc-file-logger.injectable.ts create mode 100644 packages/core/src/main/logger/ipc-file-logger.test.ts create mode 100644 packages/core/src/main/logger/ipc-file-logger.ts create mode 100644 packages/core/src/main/logger/ipc-logging-listener.injectable.ts create mode 100644 packages/core/src/main/logger/ipc-logging-listener.test.ts create mode 100644 packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts create mode 100644 packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts create mode 100644 packages/core/src/renderer/logger/ipc-transport.injectable.ts create mode 100644 packages/core/src/renderer/logger/ipc-transport.test.ts create mode 100644 packages/core/src/renderer/logger/ipc-transport.ts create mode 100644 packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts create mode 100644 packages/core/src/renderer/logger/renderer-log-file-id.test.ts diff --git a/packages/core/src/common/logger.injectable.ts b/packages/core/src/common/logger.injectable.ts index bc1c5de71b..e64978e44b 100644 --- a/packages/core/src/common/logger.injectable.ts +++ b/packages/core/src/common/logger.injectable.ts @@ -3,20 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { createLogger, format } from "winston"; import type { Logger } from "./logger"; -import { loggerTransportInjectionToken } from "./logger/transports"; +import winstonLoggerInjectable from "./winston-logger.injectable"; const loggerInjectable = getInjectable({ id: "logger", instantiate: (di): Logger => { - const baseLogger = createLogger({ - format: format.combine( - format.splat(), - format.simple(), - ), - transports: di.injectMany(loggerTransportInjectionToken), - }); + const baseLogger = di.inject(winstonLoggerInjectable); return { debug: (message, ...data) => baseLogger.debug(message, ...data), diff --git a/packages/core/src/common/logger/ipc-file-logger-channel.ts b/packages/core/src/common/logger/ipc-file-logger-channel.ts new file mode 100644 index 0000000000..8e2db7aaf4 --- /dev/null +++ b/packages/core/src/common/logger/ipc-file-logger-channel.ts @@ -0,0 +1,20 @@ +import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; + +export type IpcFileLogObject = { + fileId: string; + entry: { + level: string; + message: string; + internalMessage: string; + }; +}; + +export type IpcFileLoggerChannel = MessageChannel; + +export const ipcFileLoggerChannel: IpcFileLoggerChannel = { + id: "ipc-file-logger-channel", +}; + +export const closeIpcFileLoggerChannel: MessageChannel = { + id: "close-ipc-file-logger-channel", +}; diff --git a/packages/core/src/common/winston-logger.injectable.ts b/packages/core/src/common/winston-logger.injectable.ts new file mode 100644 index 0000000000..481d520fac --- /dev/null +++ b/packages/core/src/common/winston-logger.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { createLogger, format } from "winston"; +import { loggerTransportInjectionToken } from "./logger/transports"; + +const winstonLoggerInjectable = getInjectable({ + id: "winston-logger", + instantiate: (di) => + createLogger({ + format: format.combine(format.splat(), format.simple()), + transports: di.injectMany(loggerTransportInjectionToken), + }), +}); + +export default winstonLoggerInjectable; diff --git a/packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts b/packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts new file mode 100644 index 0000000000..606b81a181 --- /dev/null +++ b/packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; +import { getMessageChannelListenerInjectable } from "../../common/utils/channel/message-channel-listener-injection-token"; +import { + closeIpcFileLoggerChannel, +} from "../../common/logger/ipc-file-logger-channel"; + +const closeIpcFileLoggingListenerInjectable = getMessageChannelListenerInjectable({ + id: "close-ipc-file-logging", + channel: closeIpcFileLoggerChannel, + handler: (di) => (fileId) => + di + .inject(ipcFileLoggerInjectable) + .close(fileId), +}); + +export default closeIpcFileLoggingListenerInjectable; diff --git a/packages/core/src/main/logger/ipc-file-logger.injectable.ts b/packages/core/src/main/logger/ipc-file-logger.injectable.ts new file mode 100644 index 0000000000..ff26ea5c46 --- /dev/null +++ b/packages/core/src/main/logger/ipc-file-logger.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { transports } from "winston"; +import directoryForLogsInjectable from "../../common/app-paths/directory-for-logs.injectable"; +import IpcFileLogger from "./ipc-file-logger"; + +const ipcFileLoggerInjectable = getInjectable({ + id: "ipc-file-logger", + instantiate: (di) => + new IpcFileLogger( + { + dirname: di.inject(directoryForLogsInjectable), + maxsize: 1024 * 1024, + maxFiles: 2, + tailable: true, + }, + (options: transports.FileTransportOptions) => new transports.File(options) + ), +}); + +export default ipcFileLoggerInjectable; diff --git a/packages/core/src/main/logger/ipc-file-logger.test.ts b/packages/core/src/main/logger/ipc-file-logger.test.ts new file mode 100644 index 0000000000..09d2f6e9af --- /dev/null +++ b/packages/core/src/main/logger/ipc-file-logger.test.ts @@ -0,0 +1,179 @@ +import IpcFileLogger from "./ipc-file-logger"; + +describe("ipc file logger in main", () => { + let logMock: jest.Mock; + let closeMock: jest.Mock; + let createFileTransportMock: jest.Mock; + let logger: IpcFileLogger; + + beforeEach(() => { + logMock = jest.fn(); + closeMock = jest.fn(); + createFileTransportMock = jest.fn(() => ({ + log: logMock, + close: closeMock, + })); + logger = new IpcFileLogger( + { + dirname: "some-logs-dir", + maxFiles: 1, + tailable: true, + }, + createFileTransportMock + ); + }); + + it("creates a transport for new log file", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + expect(createFileTransportMock).toHaveBeenCalledWith({ + dirname: "some-logs-dir", + filename: "lens-some-log-file.log", + maxFiles: 1, + tailable: true, + }); + }); + + it("uses existing transport for log file", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + expect(createFileTransportMock).toHaveBeenCalledTimes(1); + + expect(createFileTransportMock).toHaveBeenCalledWith({ + dirname: "some-logs-dir", + filename: "lens-some-log-file.log", + maxFiles: 1, + tailable: true, + }); + }); + + it("creates separate transport for each log file", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.log({ + fileId: "some-other-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.log({ + fileId: "some-yet-another-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + expect(createFileTransportMock).toHaveBeenCalledTimes(3); + + expect(createFileTransportMock).toHaveBeenCalledWith({ + dirname: "some-logs-dir", + filename: "lens-some-log-file.log", + maxFiles: 1, + tailable: true, + }); + + expect(createFileTransportMock).toHaveBeenCalledWith({ + dirname: "some-logs-dir", + filename: "lens-some-other-log-file.log", + maxFiles: 1, + tailable: true, + }); + + expect(createFileTransportMock).toHaveBeenCalledWith({ + dirname: "some-logs-dir", + filename: "lens-some-yet-another-log-file.log", + maxFiles: 1, + tailable: true, + }); + }); + + it("logs using file transport", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "some-log-message" }, + }); + expect(logMock.mock.calls[0][0]).toEqual({ + level: "irrelevant", + message: "some-log-message", + }); + }); + + it("logs to correct files", () => { + const someLogMock = jest.fn(); + const someOthertLogMock = jest.fn(); + + createFileTransportMock.mockImplementation((options) => { + if (options.filename === "lens-some-log-file.log") { + return { log: someLogMock }; + } + if (options.filename === "lens-some-other-log-file.log") { + return { log: someOthertLogMock }; + } + return null; + }); + + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "some-log-message" }, + }); + logger.log({ + fileId: "some-other-log-file", + entry: { level: "irrelevant", message: "some-other-log-message" }, + }); + + expect(someLogMock).toHaveBeenCalledTimes(1); + expect(someLogMock.mock.calls[0][0]).toEqual({ + level: "irrelevant", + message: "some-log-message", + }); + expect(someOthertLogMock).toHaveBeenCalledTimes(1); + expect(someOthertLogMock.mock.calls[0][0]).toEqual({ + level: "irrelevant", + message: "some-other-log-message", + }); + }); + + it("closes transport (to ensure no file handles are left open)", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.close("some-log-file"); + + expect(closeMock).toHaveBeenCalled(); + }); + + it("creates a new transport once needed after closing previous", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.close("some-log-file"); + + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + expect(createFileTransportMock).toHaveBeenCalledTimes(2); + expect(logMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/core/src/main/logger/ipc-file-logger.ts b/packages/core/src/main/logger/ipc-file-logger.ts new file mode 100644 index 0000000000..8bc5694abf --- /dev/null +++ b/packages/core/src/main/logger/ipc-file-logger.ts @@ -0,0 +1,51 @@ +import type { LogEntry, transports } from "winston"; + +type IpcFileLoggerOptions = Omit; + +class IpcFileLogger { + private fileTransports = new Map(); + + constructor( + private options: IpcFileLoggerOptions, + private createNewFileTransport: ( + options: transports.FileTransportOptions + ) => transports.FileTransportInstance + ) {} + + log({ fileId, entry }: { fileId: string; entry: LogEntry }) { + const transport = this.ensureTransportForFile(fileId); + + transport?.log?.(entry, () => {}); + } + + close(fileId: string) { + const transport = this.fileTransports.get(fileId); + if (transport) { + transport.close?.(); + this.fileTransports.delete(fileId); + } + } + + closeAll() { + [...this.fileTransports.keys()].forEach((fileId) => { + this.close(fileId); + }); + } + + private ensureTransportForFile(fileId: string) { + if (this.fileTransports.has(fileId)) { + return this.fileTransports.get(fileId); + } + + const fileTransport = this.createNewFileTransport({ + ...this.options, + filename: `lens-${fileId}.log`, + }); + + this.fileTransports.set(fileId, fileTransport); + + return fileTransport; + } +} + +export default IpcFileLogger; diff --git a/packages/core/src/main/logger/ipc-logging-listener.injectable.ts b/packages/core/src/main/logger/ipc-logging-listener.injectable.ts new file mode 100644 index 0000000000..b4cb6ded94 --- /dev/null +++ b/packages/core/src/main/logger/ipc-logging-listener.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; +import { getMessageChannelListenerInjectable } from "../../common/utils/channel/message-channel-listener-injection-token"; +import { + ipcFileLoggerChannel, + IpcFileLogObject, +} from "../../common/logger/ipc-file-logger-channel"; +import { MESSAGE } from "triple-beam"; + +/** + * Winston uses symbol property for the actual message. + * + * For that to get through IPC, use the internalMessage property instead + */ +export function deserializeLogFromIpc(ipcFileLogObject: IpcFileLogObject) { + const { internalMessage, ...standardEntry } = ipcFileLogObject.entry; + return { + ...ipcFileLogObject, + entry: { + ...standardEntry, + [MESSAGE]: internalMessage, + }, + }; +} + +const ipcFileLoggingListenerInjectable = getMessageChannelListenerInjectable({ + id: "ipc-file-logging", + channel: ipcFileLoggerChannel, + handler: (di) => (ipcFileLogObject) => + di + .inject(ipcFileLoggerInjectable) + .log(deserializeLogFromIpc(ipcFileLogObject)), +}); + +export default ipcFileLoggingListenerInjectable; diff --git a/packages/core/src/main/logger/ipc-logging-listener.test.ts b/packages/core/src/main/logger/ipc-logging-listener.test.ts new file mode 100644 index 0000000000..55bb64f4c4 --- /dev/null +++ b/packages/core/src/main/logger/ipc-logging-listener.test.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { MESSAGE } from "triple-beam"; +import { deserializeLogFromIpc } from "./ipc-logging-listener.injectable"; + +describe("Ipc log deserialization", () => { + it("fills in the unique symbol message property Winston transports use internally", () => { + const logObject = { + fileId: "irrelevant", + entry: { + level: "irrelevant", + message: "some public message", + internalMessage: "some internal message", + someProperty: "irrelevant", + }, + }; + + expect(deserializeLogFromIpc(logObject)).toEqual({ + entry: { + level: "irrelevant", + message: "some public message", + [MESSAGE]: "some internal message", + someProperty: "irrelevant", + }, + fileId: "irrelevant", + }); + }); +}); diff --git a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts index c640264ee3..a9a923f860 100644 --- a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts +++ b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts @@ -12,6 +12,7 @@ import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event. import loadExtensionsInjectable from "../../load-extensions.injectable"; import loggerInjectable from "../../../../common/logger.injectable"; import showErrorNotificationInjectable from "../../../components/notifications/show-error-notification.injectable"; +import closeRendererLogFileInjectable from "../../../logger/close-renderer-log-file.injectable"; const initClusterFrameInjectable = getInjectable({ id: "init-cluster-frame", @@ -29,6 +30,7 @@ const initClusterFrameInjectable = getInjectable({ emitAppEvent: di.inject(emitAppEventInjectable), logger: di.inject(loggerInjectable), showErrorNotification: di.inject(showErrorNotificationInjectable), + closeFileLogging: di.inject(closeRendererLogFileInjectable), }); }, }); diff --git a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts index 9bd0a26a3c..38b92983a0 100644 --- a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts +++ b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts @@ -2,6 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ +import { once } from "lodash"; import type { Cluster } from "../../../../common/cluster/cluster"; import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry"; import type { ShowNotification } from "../../../components/notifications"; @@ -18,6 +19,7 @@ interface Dependencies { emitAppEvent: EmitAppEvent; logger: Logger; showErrorNotification: ShowNotification; + closeFileLogging: () => void; } const logPrefix = "[CLUSTER-FRAME]:"; @@ -30,6 +32,7 @@ export const initClusterFrame = ({ emitAppEvent, logger, showErrorNotification, + closeFileLogging, }: Dependencies) => async (unmountRoot: () => void) => { // TODO: Make catalogEntityRegistry already initialized when passed as dependency @@ -73,11 +76,15 @@ export const initClusterFrame = ({ }); }); - window.onbeforeunload = () => { + const onCloseFrame = once(() => { logger.info( `${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`, ); unmountRoot(); - }; + closeFileLogging(); + }); + + window.addEventListener("beforeunload", onCloseFrame); + window.addEventListener("pagehide", onCloseFrame); }; diff --git a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts b/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts index f1e3024d80..023c237ae6 100644 --- a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts +++ b/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts @@ -13,6 +13,7 @@ import loggerInjectable from "../../../common/logger.injectable"; import { delay } from "@k8slens/utilities"; import { broadcastMessage } from "../../../common/ipc"; import { bundledExtensionsLoaded } from "../../../common/ipc/extension-handling"; +import closeRendererLogFileInjectable from "../../logger/close-renderer-log-file.injectable"; const initRootFrameInjectable = getInjectable({ id: "init-root-frame", @@ -24,6 +25,7 @@ const initRootFrameInjectable = getInjectable({ const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable); const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable); const logger = di.inject(loggerInjectable); + const closeRendererLogFile = di.inject(closeRendererLogFileInjectable); return async (unmountRoot: () => void) => { catalogEntityRegistry.init(); @@ -60,6 +62,7 @@ const initRootFrameInjectable = getInjectable({ window.addEventListener("beforeunload", () => { logger.info("[ROOT-FRAME]: Unload app"); + closeRendererLogFile(); unmountRoot(); }); }; diff --git a/packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts b/packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts new file mode 100644 index 0000000000..8b9b725d44 --- /dev/null +++ b/packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts @@ -0,0 +1,49 @@ +import winstonLoggerInjectable from "../../common/winston-logger.injectable"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import closeRendererLogFileInjectable from "./close-renderer-log-file.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; +import type winston from "winston"; +import { SendMessageToChannel, sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; +import ipcLogTransportInjectable from "./ipc-transport.injectable"; +import type IpcLogTransport from "./ipc-transport"; + +describe("close renderer file logging", () => { + let di: DiContainer; + let sendIpcMock: SendMessageToChannel; + let winstonMock: winston.Logger; + let ipcTransportMock: IpcLogTransport; + + beforeEach(() => { + di = getDiForUnitTesting({ doGeneralOverrides: false }); + sendIpcMock = jest.fn(); + winstonMock = { + remove: jest.fn(), + } as any as winston.Logger; + ipcTransportMock = { name: "ipc-renderer-transport" } as IpcLogTransport; + + di.override(winstonLoggerInjectable, () => winstonMock); + di.override(sendMessageToChannelInjectionToken, () => sendIpcMock); + di.override(rendererLogFileIdInjectable, () => "some-log-id"); + di.override(ipcLogTransportInjectable, () => ipcTransportMock); + }); + + it("sends the ipc close message with correct log id", () => { + const closeLog = di.inject(closeRendererLogFileInjectable); + closeLog(); + + expect(sendIpcMock).toHaveBeenCalledWith( + { id: "close-ipc-file-logger-channel" }, + "some-log-id" + ); + }); + + it("removes the transport to prevent further logging to closed file", () => { + const closeLog = di.inject(closeRendererLogFileInjectable); + closeLog(); + + expect(winstonMock.remove).toHaveBeenCalledWith({ + name: "ipc-renderer-transport", + }); + }); +}); diff --git a/packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts b/packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts new file mode 100644 index 0000000000..8015708d84 --- /dev/null +++ b/packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import winstonLoggerInjectable from "../../common/winston-logger.injectable"; +import { closeIpcFileLoggerChannel } from "../../common/logger/ipc-file-logger-channel"; +import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; +import ipcLogTransportInjectable from "./ipc-transport.injectable"; + +const closeRendererLogFileInjectable = getInjectable({ + id: "close-renderer-log-file", + instantiate: (di) => { + const winstonLogger = di.inject(winstonLoggerInjectable); + const ipcLogTransport = di.inject(ipcLogTransportInjectable); + const messageToChannel = di.inject(sendMessageToChannelInjectionToken); + const fileId = di.inject(rendererLogFileIdInjectable); + + + return () => { + messageToChannel(closeIpcFileLoggerChannel, fileId); + winstonLogger.remove(ipcLogTransport); + }; + }, +}); + +export default closeRendererLogFileInjectable; diff --git a/packages/core/src/renderer/logger/ipc-transport.injectable.ts b/packages/core/src/renderer/logger/ipc-transport.injectable.ts new file mode 100644 index 0000000000..9221d7d248 --- /dev/null +++ b/packages/core/src/renderer/logger/ipc-transport.injectable.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { loggerTransportInjectionToken } from "../../common/logger/transports"; +import type winston from "winston"; +import { MESSAGE } from "triple-beam"; + +import IpcLogTransport from "./ipc-transport"; +import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import { + closeIpcFileLoggerChannel, + ipcFileLoggerChannel, + IpcFileLogObject, +} from "../../common/logger/ipc-file-logger-channel"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; + +/** + * Winston uses symbol property for the actual message. + * + * For that to get through IPC, use the internalMessage property instead + */ +function serializeLogForIpc( + fileId: string, + entry: winston.LogEntry +): IpcFileLogObject { + return { + fileId, + entry: { + level: entry.level, + message: entry.message, + internalMessage: Object.getOwnPropertyDescriptor(entry, MESSAGE)?.value, + }, + }; +} + +const ipcLogTransportInjectable = getInjectable({ + id: "renderer-file-logger-transport", + instantiate: (di) => { + const messageToChannel = di.inject(sendMessageToChannelInjectionToken); + const fileId = di.inject(rendererLogFileIdInjectable); + + return new IpcLogTransport({ + sendIpcLogMessage: (entry) => + messageToChannel( + ipcFileLoggerChannel, + serializeLogForIpc(fileId, entry) + ), + closeIpcLogging: () => + messageToChannel(closeIpcFileLoggerChannel, fileId), + handleExceptions: false, + level: "info", + }); + }, + injectionToken: loggerTransportInjectionToken, +}); + +export default ipcLogTransportInjectable; diff --git a/packages/core/src/renderer/logger/ipc-transport.test.ts b/packages/core/src/renderer/logger/ipc-transport.test.ts new file mode 100644 index 0000000000..6e7135cc41 --- /dev/null +++ b/packages/core/src/renderer/logger/ipc-transport.test.ts @@ -0,0 +1,42 @@ +import type { DiContainer } from "@ogre-tools/injectable"; +import { SendMessageToChannel, sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; +import ipcLogTransportInjectable from "./ipc-transport.injectable"; +import { MESSAGE } from "triple-beam"; + +describe("renderer log transport through ipc", () => { + let di: DiContainer; + let sendIpcMock: SendMessageToChannel; + + beforeEach(() => { + sendIpcMock = jest.fn(); + di = getDiForUnitTesting({ doGeneralOverrides: true }); + di.override(sendMessageToChannelInjectionToken, () => sendIpcMock); + di.override(rendererLogFileIdInjectable, () => "some-log-id"); + }); + + it("send serialized ipc messages on log", () => { + const logTransport = di.inject(ipcLogTransportInjectable); + logTransport.log( + { + level: "info", + message: "some log text", + [MESSAGE]: "actual winston log text", + }, + () => {} + ); + + expect(sendIpcMock).toHaveBeenCalledWith( + { id: "ipc-file-logger-channel" }, + { + entry: { + level: "info", + message: "some log text", + internalMessage: "actual winston log text", + }, + fileId: "some-log-id", + } + ); + }); +}); diff --git a/packages/core/src/renderer/logger/ipc-transport.ts b/packages/core/src/renderer/logger/ipc-transport.ts new file mode 100644 index 0000000000..a474d66eb0 --- /dev/null +++ b/packages/core/src/renderer/logger/ipc-transport.ts @@ -0,0 +1,35 @@ +import type { LogEntry } from "winston"; +import TransportStream, { TransportStreamOptions } from "winston-transport"; + +interface IpcLogTransportOptions extends TransportStreamOptions { + sendIpcLogMessage: (entry: LogEntry) => void; + closeIpcLogging: () => void; +} + +class IpcLogTransport extends TransportStream { + sendIpcLogMessage: (entry: LogEntry) => void; + closeIpcLogging: () => void; + name = "ipc-renderer-transport"; + + constructor(options: IpcLogTransportOptions) { + const { sendIpcLogMessage, closeIpcLogging, ...winstonOptions } = options; + super(winstonOptions); + + this.sendIpcLogMessage = sendIpcLogMessage; + this.closeIpcLogging = closeIpcLogging; + } + + log(logEntry: LogEntry, next: () => void) { + setImmediate(() => { + this.emit("logged", logEntry); + }); + this.sendIpcLogMessage(logEntry); + next(); + } + + close() { + this.closeIpcLogging(); + } +} + +export default IpcLogTransport; diff --git a/packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts b/packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts new file mode 100644 index 0000000000..8a1255b50e --- /dev/null +++ b/packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +import windowLocationInjectable from "../../common/k8s-api/window-location.injectable"; +import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable"; +import { getClusterIdFromHost } from "../utils"; + +const rendererLogFileIdInjectable = getInjectable({ + id: "renderer-log-file-id", + instantiate: (di) => { + let frameId: string; + const currentlyInClusterFrame = di.inject(currentlyInClusterFrameInjectable); + + if (currentlyInClusterFrame) { + const { host } = di.inject(windowLocationInjectable); + const clusterId = getClusterIdFromHost(host); + frameId = clusterId ? `cluster-${clusterId}` : "cluster"; + } else { + frameId = "main"; + } + + return `renderer-${frameId}`; + }, +}); + +export default rendererLogFileIdInjectable; diff --git a/packages/core/src/renderer/logger/renderer-log-file-id.test.ts b/packages/core/src/renderer/logger/renderer-log-file-id.test.ts new file mode 100644 index 0000000000..f220a1c7d2 --- /dev/null +++ b/packages/core/src/renderer/logger/renderer-log-file-id.test.ts @@ -0,0 +1,27 @@ +import windowLocationInjectable from "../../common/k8s-api/window-location.injectable"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; + +describe("renderer log file id", () => { + + it("clearly names log for renderer main frame", () => { + const di = getDiForUnitTesting({ doGeneralOverrides: false }); + di.override(currentlyInClusterFrameInjectable, () => false); + + const mainFileId = di.inject(rendererLogFileIdInjectable); + expect(mainFileId).toBe("renderer-main"); + }); + + it("includes cluster id in renderer log file names", () => { + const di = getDiForUnitTesting({ doGeneralOverrides: false }); + + di.override(currentlyInClusterFrameInjectable, () => true); + di.override(windowLocationInjectable, () => ({ + host: "some-cluster.lens.app", + port: "irrelevant", + })); + const clusterFileId = di.inject(rendererLogFileIdInjectable); + expect(clusterFileId).toBe("renderer-cluster-some-cluster"); + }); +});