1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Renderer file logging through IPC (#7499)

* Renderer file logging through IPC

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Remove pagehide event listener as it may cause UI to freeze

Pagehide was needed in cluster frame to better handle main frame close/reload situation. But even empty pagehide listener in cluster frame seems to freeze the UI at least on some situations (multiple clusters open).

Beforeunload is not always executed in cluster frame when main frame is reloaded/closed, leaving log files open. To fix that, `stopIpcLoggingInjectable` is introduced to close all log files.

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Remove unnecessary formatting changes

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Lint fix

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Winston logger override

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Remove usage of doGeneralOverrides as it has been removed

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Update imports to match the new base

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Remove unnecessary id

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Review improvements

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Extract beforeunload listener to injectable

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Typo fix

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Behavioural tests and log file rename

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Update messaging to work with new base

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Move files to feature

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

---------

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>
This commit is contained in:
Sami Tiilikainen 2023-04-04 15:11:06 +03:00 committed by GitHub
parent 173a667466
commit 69b132300c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 921 additions and 76 deletions

View File

@ -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),

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type winston from "winston";
import { getGlobalOverride } from "@k8slens/test-utils";
import { noop } from "@k8slens/utilities";
import winstonLoggerInjectable from "./winston-logger.injectable";
export default getGlobalOverride(winstonLoggerInjectable, () => ({
log: noop,
add: noop,
remove: noop,
clear: noop,
close: noop,
warn: noop,
debug: noop,
error: noop,
info: noop,
silly: noop,
}) as winston.Logger);

View File

@ -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;

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MessageChannel } from "@k8slens/messaging";
export interface IpcFileLogObject {
fileId: string;
entry: {
level: string;
message: string;
internalMessage: string;
};
}
export type IpcFileLoggerChannel = MessageChannel<IpcFileLogObject>;
export const ipcFileLoggerChannel: IpcFileLoggerChannel = {
id: "ipc-file-logger-channel",
};
export const closeIpcFileLoggerChannel: MessageChannel<string> = {
id: "close-ipc-file-logger-channel",
};

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getMessageChannelListenerInjectable } from "@k8slens/messaging";
import ipcFileLoggerInjectable from "./ipc-file-logger.injectable";
import {
closeIpcFileLoggerChannel,
} from "../common/ipc-file-logger-channel";
const closeIpcFileLoggingListenerInjectable = getMessageChannelListenerInjectable({
id: "close-ipc-file-logging",
channel: closeIpcFileLoggerChannel,
getHandler: (di) => {
const ipcFileLogger = di.inject(ipcFileLoggerInjectable);
return (fileId) => ipcFileLogger.close(fileId);
},
});
export default closeIpcFileLoggingListenerInjectable;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { transports } from "winston";
import { getGlobalOverride } from "@k8slens/test-utils";
import { noop } from "@k8slens/utilities";
import createIpcFileLoggerTransportInjectable from "./create-ipc-file-transport.injectable";
export default getGlobalOverride(
createIpcFileLoggerTransportInjectable,
() => () =>
({
log: noop,
close: noop,
} as typeof transports.File),
);

View File

@ -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 { transports } from "winston";
import directoryForLogsInjectable from "../../../common/app-paths/directory-for-logs.injectable";
const createIpcFileLoggerTransportInjectable = getInjectable({
id: "create-ipc-file-logger-transport",
instantiate: (di) => {
const options = {
dirname: di.inject(directoryForLogsInjectable),
maxsize: 1024 * 1024,
maxFiles: 2,
tailable: true,
};
return (fileId: string) =>
new transports.File({
...options,
filename: `lens-${fileId}.log`,
});
},
causesSideEffects: true,
});
export default createIpcFileLoggerTransportInjectable;

View File

@ -0,0 +1,55 @@
/**
* 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 { getOrInsertWith } from "@k8slens/utilities";
import type { LogEntry, transports } from "winston";
import createIpcFileLoggerTransportInjectable from "./create-ipc-file-transport.injectable";
export interface IpcFileLogger {
log: (fileLog: { fileId: string; entry: LogEntry }) => void;
close: (fileId: string) => void;
closeAll: () => void;
}
const ipcFileLoggerInjectable = getInjectable({
id: "ipc-file-logger",
instantiate: (di): IpcFileLogger => {
const createIpcFileTransport = di.inject(createIpcFileLoggerTransportInjectable);
const fileTransports = new Map<string, transports.FileTransportInstance>();
function log({ fileId, entry }: { fileId: string; entry: LogEntry }) {
const transport = getOrInsertWith(
fileTransports,
fileId,
() => createIpcFileTransport(fileId),
);
transport?.log?.(entry, () => {});
}
function close(fileId: string) {
const transport = fileTransports.get(fileId);
if (transport) {
transport.close?.();
fileTransports.delete(fileId);
}
}
function closeAll() {
for (const fileId of fileTransports.keys()) {
close(fileId);
}
}
return {
log,
close,
closeAll,
};
},
});
export default ipcFileLoggerInjectable;

View File

@ -0,0 +1,160 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
import createIpcFileLoggerTransportInjectable from "./create-ipc-file-transport.injectable";
import type { IpcFileLogger } from "./ipc-file-logger.injectable";
import ipcFileLoggerInjectable from "./ipc-file-logger.injectable";
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,
}));
const di = getDiForUnitTesting();
di.override(createIpcFileLoggerTransportInjectable, () => createFileTransportMock);
logger = di.inject(ipcFileLoggerInjectable);
});
it("creates a transport for new log file", () => {
logger.log({
fileId: "some-log-file",
entry: { level: "irrelevant", message: "irrelevant" },
});
expect(createFileTransportMock).toHaveBeenCalledWith("some-log-file");
});
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("some-log-file");
});
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("some-log-file");
expect(createFileTransportMock).toHaveBeenCalledWith("some-other-log-file");
expect(createFileTransportMock).toHaveBeenCalledWith("some-yet-another-log-file");
});
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((fileId: string) => {
if (fileId === "some-log-file") {
return { log: someLogMock };
}
if (fileId === "some-other-log-file") {
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);
});
});

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getMessageChannelListenerInjectable } from "@k8slens/messaging";
import type { IpcFileLogObject } from "../common/ipc-file-logger-channel";
import { ipcFileLoggerChannel } from "../common/ipc-file-logger-channel";
import { MESSAGE } from "triple-beam";
import ipcFileLoggerInjectable from "./ipc-file-logger.injectable";
/**
* 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,
getHandler: (di) => {
const ipcFileLogger = di.inject(ipcFileLoggerInjectable);
return (ipcFileLogObject) =>
ipcFileLogger.log(deserializeLogFromIpc(ipcFileLogObject));
},
});
export default ipcFileLoggingListenerInjectable;

View File

@ -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",
});
});
});

View File

@ -0,0 +1,27 @@
/**
* 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 { beforeQuitOfFrontEndInjectionToken } from "../../../main/start-main-application/runnable-tokens/phases";
import ipcFileLoggerInjectable from "./ipc-file-logger.injectable";
const stopIpcLoggingInjectable = getInjectable({
id: "stop-ipc-logging",
instantiate: (di) => {
const ipcFileLogger = di.inject(ipcFileLoggerInjectable);
return {
run: () => {
ipcFileLogger.closeAll();
return undefined;
},
};
},
injectionToken: beforeQuitOfFrontEndInjectionToken,
});
export default stopIpcLoggingInjectable;

View File

@ -0,0 +1,131 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import winstonLoggerInjectable from "../../common/winston-logger.injectable";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type winston from "winston";
import { MESSAGE } from "triple-beam";
import { noop } from "@k8slens/utilities";
import windowLocationInjectable from "../../common/k8s-api/window-location.injectable";
import closeRendererLogFileInjectable from "./renderer/close-renderer-log-file.injectable";
import createIpcFileLoggerTransportInjectable from "./main/create-ipc-file-transport.injectable";
import browserLoggerTransportInjectable from "../../renderer/logger/browser-transport.injectable";
import { runInAction } from "mobx";
describe("Population of logs to a file", () => {
let builder: ApplicationBuilder;
let windowDi: DiContainer;
let logWarningInRenderer: (message: string, ...args: any) => void;
let frameSpecificWinstonLogInMainMock: jest.Mock;
let frameSpecificCloseLogInMainMock: jest.Mock;
async function setUpTestApplication({
testFileId,
isClusterFrame,
}: {
testFileId: string;
isClusterFrame: boolean;
}) {
builder = getApplicationBuilder();
if (isClusterFrame) {
builder.setEnvironmentToClusterFrame();
}
frameSpecificWinstonLogInMainMock = jest.fn();
frameSpecificCloseLogInMainMock = jest.fn();
builder.beforeApplicationStart(({ mainDi }) => {
mainDi.override(
createIpcFileLoggerTransportInjectable,
() => (fileId: string) =>
({
log:
fileId === testFileId ? frameSpecificWinstonLogInMainMock : noop,
close:
fileId === testFileId ? frameSpecificCloseLogInMainMock : noop,
} as unknown as winston.transport),
);
});
builder.beforeWindowStart(({ windowDi }) => {
windowDi.unoverride(winstonLoggerInjectable);
// Now that we have the actual winston logger in use, let's not be noisy and deregister console transport
runInAction(() => {
windowDi.deregister(browserLoggerTransportInjectable);
});
if (isClusterFrame) {
windowDi.override(windowLocationInjectable, () => ({
host: "some-cluster.some-domain.localhost:irrelevant",
port: "irrelevant",
}));
}
});
await builder.render();
windowDi = builder.applicationWindow.only.di;
const winstonLogger = windowDi.inject(winstonLoggerInjectable);
logWarningInRenderer = winstonLogger.warn;
}
describe("given in root frame", () => {
beforeEach(async () => {
await setUpTestApplication({
testFileId: "renderer-root-frame",
isClusterFrame: false,
});
});
it("when logging a warning in renderer, writes to frame specific Winston log", async () => {
logWarningInRenderer("some-warning");
expect(frameSpecificWinstonLogInMainMock).toHaveBeenCalledWith(
{
level: "warn",
message: "some-warning",
[MESSAGE]: "warn: some-warning",
},
expect.any(Function),
);
});
it("when closing the renderer frame, closes specific log transport in main", () => {
const closeRendererLogFile = windowDi.inject(
closeRendererLogFileInjectable,
);
// Log something to create the transport to be closed
logWarningInRenderer("irrelevant");
closeRendererLogFile();
expect(frameSpecificCloseLogInMainMock).toHaveBeenCalled();
});
});
describe("given in cluster frame", () => {
beforeEach(async () => {
await setUpTestApplication({
testFileId: "renderer-cluster-some-cluster-frame",
isClusterFrame: true,
});
});
it("when logging a warning in renderer, writes to frame specific Winston log", async () => {
logWarningInRenderer("some-warning");
expect(frameSpecificWinstonLogInMainMock).toHaveBeenCalledWith(
{
level: "warn",
message: "some-warning",
[MESSAGE]: "warn: some-warning",
},
expect.any(Function),
);
});
});
});

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type winston from "winston";
import type { SendMessageToChannel } from "@k8slens/messaging";
import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
import type { DiContainer } from "@ogre-tools/injectable";
import winstonLoggerInjectable from "../../../common/winston-logger.injectable";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import closeRendererLogFileInjectable from "./close-renderer-log-file.injectable";
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();
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("removes the transport to prevent further logging to closed file", () => {
const closeLog = di.inject(closeRendererLogFileInjectable);
closeLog();
expect(winstonMock.remove).toHaveBeenCalledWith({
name: "ipc-renderer-transport",
});
});
});

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
import { getInjectable } from "@ogre-tools/injectable";
import winstonLoggerInjectable from "../../../common/winston-logger.injectable";
import { closeIpcFileLoggerChannel } from "../common/ipc-file-logger-channel";
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;

View File

@ -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 type { IpcFileLogObject } from "../common/ipc-file-logger-channel";
import {
closeIpcFileLoggerChannel,
ipcFileLoggerChannel,
} from "../common/ipc-file-logger-channel";
import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable";
import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
/**
* 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;

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { MESSAGE } from "triple-beam";
import type { SendMessageToChannel } from "@k8slens/messaging";
import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable";
import ipcLogTransportInjectable from "./ipc-transport.injectable";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
describe("renderer log transport through ipc", () => {
let di: DiContainer;
let sendIpcMock: SendMessageToChannel;
beforeEach(() => {
sendIpcMock = jest.fn();
di = getDiForUnitTesting();
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",
},
);
});
});

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { LogEntry } from "winston";
import type { TransportStreamOptions } from "winston-transport";
import TransportStream 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;

View File

@ -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 "../../../renderer/routes/currently-in-cluster-frame.injectable";
import { getClusterIdFromHost } from "../../../common/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 = `cluster-${clusterId}`;
} else {
frameId = "root";
}
return `renderer-${frameId}-frame`;
},
});
export default rendererLogFileIdInjectable;

View File

@ -7,8 +7,8 @@ import { transports } from "winston";
import directoryForLogsInjectable from "../../common/app-paths/directory-for-logs.injectable";
import { loggerTransportInjectionToken } from "../../common/logger/transports";
const fileLoggerTranportInjectable = getInjectable({
id: "file-logger-tranport",
const fileLoggerTransportInjectable = getInjectable({
id: "file-logger-transport",
instantiate: (di) => new transports.File({
handleExceptions: false,
level: "debug",
@ -26,4 +26,4 @@ const fileLoggerTranportInjectable = getInjectable({
decorable: false,
});
export default fileLoggerTranportInjectable;
export default fileLoggerTransportInjectable;

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { unmountComponentAtNode } from "react-dom";
import { getInjectable } from "@ogre-tools/injectable";
import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable";
import { beforeFrameStartsSecondInjectionToken } from "../tokens";
import loggerInjectable from "../../../common/logger.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import frameRoutingIdInjectable from "../../frames/cluster-frame/init-cluster-frame/frame-routing-id/frame-routing-id.injectable";
import closeRendererLogFileInjectable from "../../../features/population-of-logs-to-a-file/renderer/close-renderer-log-file.injectable";
const listenUnloadInjectable = getInjectable({
id: "listen-unload",
instantiate: (di) => ({
run: () => {
const closeRendererLogFile = di.inject(closeRendererLogFileInjectable);
const isClusterFrame = di.inject(currentlyInClusterFrameInjectable);
const logger = di.inject(loggerInjectable);
window.addEventListener("beforeunload", () => {
if (isClusterFrame) {
const hostedCluster = di.inject(hostedClusterInjectable);
const frameRoutingId = di.inject(frameRoutingIdInjectable);
logger.info(
`[CLUSTER-FRAME] Unload dashboard, clusterId=${hostedCluster?.id}, frameId=${frameRoutingId}`,
);
} else {
logger.info("[ROOT-FRAME]: Unload app");
}
closeRendererLogFile();
const rootElem = document.getElementById("app");
if (rootElem) {
unmountComponentAtNode(rootElem);
}
});
},
}),
injectionToken: beforeFrameStartsSecondInjectionToken,
});
export default listenUnloadInjectable;

View File

@ -5,7 +5,6 @@
import "./components/app.scss";
import { unmountComponentAtNode } from "react-dom";
import type {
DiContainerForInjection,
} from "@ogre-tools/injectable";
@ -35,7 +34,7 @@ export async function bootstrap(di: DiContainerForInjection) {
}
try {
await initializeApp(() => unmountComponentAtNode(rootElem));
await initializeApp();
} catch (error) {
console.error(`[BOOTSTRAP]: view initialization error: ${error}`, {
origin: location.href,

View File

@ -22,7 +22,8 @@ interface Dependencies {
const logPrefix = "[CLUSTER-FRAME]:";
export const initClusterFrame = ({
export const initClusterFrame =
({
hostedCluster,
loadExtensions,
catalogEntityRegistry,
@ -31,7 +32,7 @@ export const initClusterFrame = ({
logger,
showErrorNotification,
}: Dependencies) =>
async (unmountRoot: () => void) => {
async () => {
// TODO: Make catalogEntityRegistry already initialized when passed as dependency
catalogEntityRegistry.init();
@ -48,15 +49,11 @@ export const initClusterFrame = ({
// Note that the Catalog might still have unprocessed entities until the extensions are fully loaded.
when(
() => catalogEntityRegistry.items.get().length > 0,
() =>
loadExtensions(),
() => loadExtensions(),
{
timeout: 15_000,
onError: (error) => {
logger.warn(
"[CLUSTER-FRAME]: error from activeEntity when()",
error,
);
logger.warn("[CLUSTER-FRAME]: error from activeEntity when()", error);
showErrorNotification("Failed to get KubernetesCluster for this view. Extensions will not be loaded.");
},
@ -72,12 +69,4 @@ export const initClusterFrame = ({
},
});
});
window.onbeforeunload = () => {
logger.info(
`${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`,
);
unmountRoot();
};
};

View File

@ -9,7 +9,6 @@ import lensProtocolRouterRendererInjectable from "../../protocol-handler/lens-pr
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
import registerIpcListenersInjectable from "../../ipc/register-ipc-listeners.injectable";
import loadExtensionsInjectable from "../load-extensions.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import { delay } from "@k8slens/utilities";
import { broadcastMessage } from "../../../common/ipc";
import { bundledExtensionsLoaded } from "../../../common/ipc/extension-handling";
@ -23,9 +22,8 @@ const initRootFrameInjectable = getInjectable({
const bindProtocolAddRouteHandlers = di.inject(bindProtocolAddRouteHandlersInjectable);
const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const logger = di.inject(loggerInjectable);
return async (unmountRoot: () => void) => {
return async () => {
catalogEntityRegistry.init();
try {
@ -56,12 +54,6 @@ const initRootFrameInjectable = getInjectable({
window.addEventListener("online", () => broadcastMessage("network:online"));
registerIpcListeners();
window.addEventListener("beforeunload", () => {
logger.info("[ROOT-FRAME]: Unload app");
unmountRoot();
});
};
},
});