Skip to content

Commit

Permalink
perf: refactor PluginManager
Browse files Browse the repository at this point in the history
  • Loading branch information
theogravity committed May 8, 2024
1 parent d9729b3 commit d6319cf
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-bugs-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"loglayer": patch
---

Refactor `PluginManager` for performance improvements.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 16 additions & 13 deletions src/LogLayer.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { LogBuilder } from "./LogBuilder";
import { PluginManager } from "./plugins/PluginManager";
import type {
ErrorDataType,
ErrorOnlyOpts,
ILogLayer,
LogLayerConfig,
LogLayerContextConfig,
LogLayerErrorConfig,
LogLayerMetadataConfig,
LoggerLibrary,
MessageDataType,
import {
type ErrorDataType,
type ErrorOnlyOpts,
type ILogLayer,
type LogLayerConfig,
type LogLayerContextConfig,
type LogLayerErrorConfig,
type LogLayerMetadataConfig,
type LogLayerPlugin,
LogLevel,
type LoggerLibrary,
LoggerType,
type MessageDataType,
PluginCallbackType,
} from "./types";
import { type LogLayerPlugin, LogLevel, LoggerType } from "./types";

interface FormatLogParams {
logLevel: LogLevel;
Expand Down Expand Up @@ -432,14 +435,14 @@ export class LogLayer<ExternalLogger extends LoggerLibrary = LoggerLibrary, Erro
}
}

if (this.pluginManager.hasPlugins()) {
if (this.pluginManager.hasPlugins(PluginCallbackType.onBeforeDataOut)) {
d = this.pluginManager.runOnBeforeDataOut({
data: hasObjData ? d : undefined,
logLevel,
});
}

if (this.pluginManager.hasPlugins()) {
if (this.pluginManager.hasPlugins(PluginCallbackType.shouldSendToLogger)) {
const shouldSend = this.pluginManager.runShouldSendToLogger({
messages: [...params],
data: hasObjData ? d : undefined,
Expand Down
134 changes: 117 additions & 17 deletions src/plugins/PluginManager.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,97 @@
import type { LogLayerPlugin, PluginBeforeDataOutParams, PluginShouldSendToLoggerParams } from "../types";
import {
type LogLayerPlugin,
type PluginBeforeDataOutParams,
PluginCallbackType,
type PluginShouldSendToLoggerParams,
} from "../types";

const CALLBACK_LIST = [
PluginCallbackType.onErrorCalled,
PluginCallbackType.onBeforeDataOut,
PluginCallbackType.onMetadataCalled,
PluginCallbackType.shouldSendToLogger,
];

export class PluginManager<Data extends Record<string, any> = Record<string, any>> {
private plugins: Array<LogLayerPlugin>;
private idToPlugin: Record<string, LogLayerPlugin>;
// Indexes for each plugin type
private onBeforeDataOut: Array<string> = [];
private shouldSendToLogger: Array<string> = [];
private onMetadataCalled: Array<string> = [];
private onErrorCalled: Array<string> = [];

constructor(plugins: Array<LogLayerPlugin>) {
this.plugins = plugins;
this.idToPlugin = {};
this.mapPlugins(plugins);
this.indexPlugins();
}

hasPlugins() {
return this.plugins.length > 0;
private mapPlugins(plugins: Array<LogLayerPlugin>) {
for (const plugin of plugins) {
if (!plugin.id) {
plugin.id = new Date().getTime().toString() + Math.random().toString();
}

this.idToPlugin[plugin.id] = plugin;
}
}

private indexPlugins() {
this.onBeforeDataOut = [];
this.shouldSendToLogger = [];
this.onMetadataCalled = [];
this.onErrorCalled = [];

for (const plugin of Object.values(this.idToPlugin)) {
if (plugin.disabled) {
return;
}

for (const callback of CALLBACK_LIST) {
// If the callback is defined, add the plugin id to the callback list
if (plugin[callback] && plugin.id) {
this[callback].push(plugin.id);
}
}
}
}

hasPlugins(callbackType: PluginCallbackType) {
return this[callbackType].length > 0;
}

countPlugins() {
return Object.keys(this.idToPlugin).length;
}

addPlugins(plugins: Array<LogLayerPlugin>) {
this.plugins.push(...plugins);
this.mapPlugins(plugins);
this.indexPlugins();
}

enablePlugin(id: string) {
const plugin = this.plugins.find((plugin) => plugin.id === id);
const plugin = this.idToPlugin[id];

if (plugin) {
plugin.disabled = false;
}

this.indexPlugins();
}

disablePlugin(id: string) {
const plugin = this.plugins.find((plugin) => plugin.id === id);
const plugin = this.idToPlugin[id];

if (plugin) {
plugin.disabled = true;
}

this.indexPlugins();
}

removePlugin(id: string) {
this.plugins = this.plugins.filter((plugin) => plugin.id !== id);
delete this.idToPlugin[id];
this.indexPlugins();
}

/**
Expand All @@ -41,8 +100,10 @@ export class PluginManager<Data extends Record<string, any> = Record<string, any
runOnBeforeDataOut(params: PluginBeforeDataOutParams): Record<string, any> | undefined {
const initialData = { ...params }; // Make a shallow copy of params to avoid direct modification

for (const plugin of this.plugins) {
if (!plugin.disabled && plugin.onBeforeDataOut) {
for (const pluginId of this.onBeforeDataOut) {
const plugin = this.idToPlugin[pluginId];

if (plugin.onBeforeDataOut) {
const result = plugin.onBeforeDataOut({
data: initialData.data,
logLevel: initialData.logLevel,
Expand All @@ -66,15 +127,54 @@ export class PluginManager<Data extends Record<string, any> = Record<string, any
* Runs plugins that define shouldSendToLogger. Any plugin that returns false will prevent the message from being sent to the logger.
*/
runShouldSendToLogger(params: PluginShouldSendToLoggerParams) {
return !this.plugins.some((plugin) => {
// Send to logger if the plugin is disabled or the plugin does not have a 'shouldSendToLogger' function.
if (plugin.disabled || !plugin.shouldSendToLogger) {
return false;
}
return !this.shouldSendToLogger.some((pluginId) => {
const plugin = this.idToPlugin[pluginId];

// Return the negation of 'shouldSendToLogger' because 'some' will stop on true,
// and we stop on an explicit false return value from 'shouldSendToLogger'.
return !plugin.shouldSendToLogger(params);
return !plugin.shouldSendToLogger!(params);
});
}

/**
* Runs plugins that define onMetadataCalled.
*/
runOnMetadataCalled(metadata: Record<string, any>): Record<string, any> | null {
let data: Record<string, any> = metadata;

for (const pluginId of this.onMetadataCalled) {
const plugin = this.idToPlugin[pluginId];

const result = plugin.onMetadataCalled!(data);

if (result) {
data = result;
} else {
return null;
}
}

return data;
}

/**
* Runs plugins that define onErrorCalled.
*/
runOnErrorCalled(error: any) {
let data: any = null;

for (const pluginId of this.onErrorCalled) {
const plugin = this.idToPlugin[pluginId];

const result = plugin.onErrorCalled!(data);

if (result) {
data = result;
} else {
return null;
}
}

return data;
}
}
33 changes: 8 additions & 25 deletions src/plugins/__tests__/PluginManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type LogLayerPlugin,
LogLevel,
type PluginBeforeDataOutParams,
PluginCallbackType,
type PluginShouldSendToLoggerParams,
} from "../../types";
import { PluginManager } from "../PluginManager";
Expand All @@ -28,7 +29,8 @@ describe("PluginManager", () => {
});

it("should initialize with passed plugins", () => {
expect(pluginManager.hasPlugins()).toBe(true);
expect(pluginManager.hasPlugins(PluginCallbackType.onBeforeDataOut)).toBe(true);
expect(pluginManager.hasPlugins(PluginCallbackType.shouldSendToLogger)).toBe(true);
});

it("adds plugins to the list", () => {
Expand All @@ -38,8 +40,10 @@ describe("PluginManager", () => {
};
pluginManager.addPlugins([newPlugin]);

expect(pluginManager.hasPlugins()).toBe(true);
expect(pluginManager["plugins"].length).toBe(3);
expect(pluginManager.hasPlugins(PluginCallbackType.onBeforeDataOut)).toBe(true);
expect(pluginManager.hasPlugins(PluginCallbackType.shouldSendToLogger)).toBe(true);
expect(pluginManager["onBeforeDataOut"].length).toBe(3);
expect(pluginManager["shouldSendToLogger"].length).toBe(3);
});

it("runs onBeforeDataOut and modifies data correctly", () => {
Expand Down Expand Up @@ -76,26 +80,6 @@ describe("PluginManager", () => {
expect(plugins[1].shouldSendToLogger).toHaveBeenCalledTimes(1);
});

it("runs shouldSendToLogger and returns true if not defined", () => {
const params: PluginShouldSendToLoggerParams = {
logLevel: LogLevel.error,
messages: ["Test message"],
data: { key: "value" },
};

// 0th plugin returns true
plugins[1].shouldSendToLogger = undefined;

const shouldSend = pluginManager.runShouldSendToLogger(params);
expect(shouldSend).toBe(true);

plugins[0].shouldSendToLogger = undefined;
plugins[1].shouldSendToLogger = undefined;

const shouldSend2 = pluginManager.runShouldSendToLogger(params);
expect(shouldSend2).toBe(true);
});

it("disables a plugin", () => {
pluginManager.disablePlugin(plugins[0].id!);
expect(plugins[0].disabled).toBe(true);
Expand All @@ -109,7 +93,6 @@ describe("PluginManager", () => {

it("removes a plugin", () => {
pluginManager.removePlugin(plugins[0].id!);
expect(pluginManager.hasPlugins()).toBe(true);
expect(pluginManager["plugins"].length).toBe(1);
expect(pluginManager.countPlugins()).toBe(1);
});
});
27 changes: 27 additions & 0 deletions src/types/plugins.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,31 @@ export interface LogLayerPlugin {
* @returns boolean If true, sends data to the logger, if false does not.
*/
shouldSendToLogger?(params: PluginShouldSendToLoggerParams): boolean;
/**
* Called when withMetadata() or metadataOnly() is called. This allows you to modify the metadata before it is sent to the destination logging library.
*
* If null is returned, then no metadata will be sent to the destination logging library.
*
* @returns [Object] The metadata object to be sent to the destination logging library.
*/
onMetadataCalled?: (metadata: Record<string, any>) => Record<string, any> | null;
/**
* Called when withError() or errorOnly() is called. This allows you to modify
* the error before it is sent to the destination logging library.
*
* If null is returned, then no error will be sent to the destination logging library.
*
* @returns The error object to be sent to the destination logging library.
*/
onErrorCalled?: (error: any) => any | null;
}

/**
* List of plugin callbacks that can be called by the plugin manager.
*/
export enum PluginCallbackType {
onBeforeDataOut = "onBeforeDataOut",
shouldSendToLogger = "shouldSendToLogger",
onMetadataCalled = "onMetadataCalled",
onErrorCalled = "onErrorCalled",
}

0 comments on commit d6319cf

Please sign in to comment.