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:
parent
173a667466
commit
69b132300c
@ -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),
|
||||
|
||||
@ -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);
|
||||
18
packages/core/src/common/winston-logger.injectable.ts
Normal file
18
packages/core/src/common/winston-logger.injectable.ts
Normal 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;
|
||||
@ -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",
|
||||
};
|
||||
@ -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;
|
||||
@ -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),
|
||||
);
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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",
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
};
|
||||
};
|
||||
|
||||
@ -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();
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user