Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Renderer file logging through IPC #7499

Merged
merged 14 commits into from
Apr 4, 2023
Merged
11 changes: 2 additions & 9 deletions packages/core/src/common/logger.injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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 changes: 18 additions & 0 deletions packages/core/src/common/winston-logger.injectable.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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",
};
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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),
);
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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);
});
});