mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Renderer file logging through IPC
Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>
This commit is contained in:
parent
47796228d0
commit
d3cc345cde
@ -3,20 +3,13 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { createLogger, format } from "winston";
|
|
||||||
import type { Logger } from "./logger";
|
import type { Logger } from "./logger";
|
||||||
import { loggerTransportInjectionToken } from "./logger/transports";
|
import winstonLoggerInjectable from "./winston-logger.injectable";
|
||||||
|
|
||||||
const loggerInjectable = getInjectable({
|
const loggerInjectable = getInjectable({
|
||||||
id: "logger",
|
id: "logger",
|
||||||
instantiate: (di): Logger => {
|
instantiate: (di): Logger => {
|
||||||
const baseLogger = createLogger({
|
const baseLogger = di.inject(winstonLoggerInjectable);
|
||||||
format: format.combine(
|
|
||||||
format.splat(),
|
|
||||||
format.simple(),
|
|
||||||
),
|
|
||||||
transports: di.injectMany(loggerTransportInjectionToken),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
debug: (message, ...data) => baseLogger.debug(message, ...data),
|
debug: (message, ...data) => baseLogger.debug(message, ...data),
|
||||||
|
|||||||
20
packages/core/src/common/logger/ipc-file-logger-channel.ts
Normal file
20
packages/core/src/common/logger/ipc-file-logger-channel.ts
Normal file
@ -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<IpcFileLogObject>;
|
||||||
|
|
||||||
|
export const ipcFileLoggerChannel: IpcFileLoggerChannel = {
|
||||||
|
id: "ipc-file-logger-channel",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const closeIpcFileLoggerChannel: MessageChannel<string> = {
|
||||||
|
id: "close-ipc-file-logger-channel",
|
||||||
|
};
|
||||||
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,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;
|
||||||
24
packages/core/src/main/logger/ipc-file-logger.injectable.ts
Normal file
24
packages/core/src/main/logger/ipc-file-logger.injectable.ts
Normal file
@ -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;
|
||||||
179
packages/core/src/main/logger/ipc-file-logger.test.ts
Normal file
179
packages/core/src/main/logger/ipc-file-logger.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
51
packages/core/src/main/logger/ipc-file-logger.ts
Normal file
51
packages/core/src/main/logger/ipc-file-logger.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import type { LogEntry, transports } from "winston";
|
||||||
|
|
||||||
|
type IpcFileLoggerOptions = Omit<transports.FileTransportOptions, "filename">;
|
||||||
|
|
||||||
|
class IpcFileLogger {
|
||||||
|
private fileTransports = new Map<string, transports.FileTransportInstance>();
|
||||||
|
|
||||||
|
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;
|
||||||
@ -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;
|
||||||
31
packages/core/src/main/logger/ipc-logging-listener.test.ts
Normal file
31
packages/core/src/main/logger/ipc-logging-listener.test.ts
Normal 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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -12,6 +12,7 @@ import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.
|
|||||||
import loadExtensionsInjectable from "../../load-extensions.injectable";
|
import loadExtensionsInjectable from "../../load-extensions.injectable";
|
||||||
import loggerInjectable from "../../../../common/logger.injectable";
|
import loggerInjectable from "../../../../common/logger.injectable";
|
||||||
import showErrorNotificationInjectable from "../../../components/notifications/show-error-notification.injectable";
|
import showErrorNotificationInjectable from "../../../components/notifications/show-error-notification.injectable";
|
||||||
|
import closeRendererLogFileInjectable from "../../../logger/close-renderer-log-file.injectable";
|
||||||
|
|
||||||
const initClusterFrameInjectable = getInjectable({
|
const initClusterFrameInjectable = getInjectable({
|
||||||
id: "init-cluster-frame",
|
id: "init-cluster-frame",
|
||||||
@ -29,6 +30,7 @@ const initClusterFrameInjectable = getInjectable({
|
|||||||
emitAppEvent: di.inject(emitAppEventInjectable),
|
emitAppEvent: di.inject(emitAppEventInjectable),
|
||||||
logger: di.inject(loggerInjectable),
|
logger: di.inject(loggerInjectable),
|
||||||
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
||||||
|
closeFileLogging: di.inject(closeRendererLogFileInjectable),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* 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 { Cluster } from "../../../../common/cluster/cluster";
|
||||||
import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry";
|
import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry";
|
||||||
import type { ShowNotification } from "../../../components/notifications";
|
import type { ShowNotification } from "../../../components/notifications";
|
||||||
@ -18,6 +19,7 @@ interface Dependencies {
|
|||||||
emitAppEvent: EmitAppEvent;
|
emitAppEvent: EmitAppEvent;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
showErrorNotification: ShowNotification;
|
showErrorNotification: ShowNotification;
|
||||||
|
closeFileLogging: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logPrefix = "[CLUSTER-FRAME]:";
|
const logPrefix = "[CLUSTER-FRAME]:";
|
||||||
@ -30,6 +32,7 @@ export const initClusterFrame = ({
|
|||||||
emitAppEvent,
|
emitAppEvent,
|
||||||
logger,
|
logger,
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
|
closeFileLogging,
|
||||||
}: Dependencies) =>
|
}: Dependencies) =>
|
||||||
async (unmountRoot: () => void) => {
|
async (unmountRoot: () => void) => {
|
||||||
// TODO: Make catalogEntityRegistry already initialized when passed as dependency
|
// TODO: Make catalogEntityRegistry already initialized when passed as dependency
|
||||||
@ -73,11 +76,15 @@ export const initClusterFrame = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.onbeforeunload = () => {
|
const onCloseFrame = once(() => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`,
|
`${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
unmountRoot();
|
unmountRoot();
|
||||||
};
|
closeFileLogging();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", onCloseFrame);
|
||||||
|
window.addEventListener("pagehide", onCloseFrame);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import loggerInjectable from "../../../common/logger.injectable";
|
|||||||
import { delay } from "@k8slens/utilities";
|
import { delay } from "@k8slens/utilities";
|
||||||
import { broadcastMessage } from "../../../common/ipc";
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
import { bundledExtensionsLoaded } from "../../../common/ipc/extension-handling";
|
import { bundledExtensionsLoaded } from "../../../common/ipc/extension-handling";
|
||||||
|
import closeRendererLogFileInjectable from "../../logger/close-renderer-log-file.injectable";
|
||||||
|
|
||||||
const initRootFrameInjectable = getInjectable({
|
const initRootFrameInjectable = getInjectable({
|
||||||
id: "init-root-frame",
|
id: "init-root-frame",
|
||||||
@ -24,6 +25,7 @@ const initRootFrameInjectable = getInjectable({
|
|||||||
const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable);
|
const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable);
|
||||||
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
|
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
|
const closeRendererLogFile = di.inject(closeRendererLogFileInjectable);
|
||||||
|
|
||||||
return async (unmountRoot: () => void) => {
|
return async (unmountRoot: () => void) => {
|
||||||
catalogEntityRegistry.init();
|
catalogEntityRegistry.init();
|
||||||
@ -60,6 +62,7 @@ const initRootFrameInjectable = getInjectable({
|
|||||||
window.addEventListener("beforeunload", () => {
|
window.addEventListener("beforeunload", () => {
|
||||||
logger.info("[ROOT-FRAME]: Unload app");
|
logger.info("[ROOT-FRAME]: Unload app");
|
||||||
|
|
||||||
|
closeRendererLogFile();
|
||||||
unmountRoot();
|
unmountRoot();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
42
packages/core/src/renderer/logger/ipc-transport.test.ts
Normal file
42
packages/core/src/renderer/logger/ipc-transport.test.ts
Normal file
@ -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",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
35
packages/core/src/renderer/logger/ipc-transport.ts
Normal file
35
packages/core/src/renderer/logger/ipc-transport.ts
Normal file
@ -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;
|
||||||
@ -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;
|
||||||
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user