From ccb99841b95664b7a17d786bf5032b60517f610a Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 30 Oct 2021 15:57:55 +0400 Subject: [PATCH 01/23] Initial implementation of logging utilities. --- src/domain/shared/Log.ts | 235 ++++++++++++++++++++++++++++++++++ tests/Log.unit.test.ts | 269 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 504 insertions(+) create mode 100644 src/domain/shared/Log.ts create mode 100644 tests/Log.unit.test.ts diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts new file mode 100644 index 00000000..58affd77 --- /dev/null +++ b/src/domain/shared/Log.ts @@ -0,0 +1,235 @@ +// +// Log.ts +// +// Created by Nshan G. on 29 Oct 2021. +// Copyright 2021 Vircadia contributors. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +export enum LogLevel { + DEBUG, + DEFAULT, + INFO, + WARNING, + ERROR, +} + +export const allLogLevels = [ + LogLevel.DEBUG, + LogLevel.DEFAULT, + LogLevel.INFO, + LogLevel.WARNING, + LogLevel.ERROR, +] as const; + +type LogFunction = (message: string, messageType?: string) => void; + +/*@devdoc + * The LoggerContext is an interface for configuration of the output of the Logger class. + * @interface LoggerContext + * + */ +export interface LoggerContext { + getFunction(level: LogLevel): LogFunction | undefined; +} + + +/*@devdoc + * The StringLoggerContext is a logger configuration for logging to the dev console. + * + * @class Logger + */ +export class ConsoleLoggerContext implements LoggerContext { + + static #_typeFirst(func: (fisrs:string, second?:string) => void) { + return (message: string, messageType?: string) => messageType ? func(`[${messageType}]`, message) : func(message); ; + } + // it is necessary to capture the console object here, for things like jest's mocks to work, + // hence the no-op looking lambda wrappers + static #_functions = new Map([ + [LogLevel.DEFAULT, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.log(...params))], + [LogLevel.DEBUG, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.debug(...params))], + [LogLevel.INFO, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.info(...params))], + [LogLevel.WARNING, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.warn(...params))], + [LogLevel.ERROR, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.error(...params))] + ]); + + getFunction(level: LogLevel) + { + return ConsoleLoggerContext.#_functions.get(level); + } +} + +/*@devdoc + * The StringLoggerContext is a configuration for logging to a string. + * + * @class Logger + */ +export class StringLoggerContext implements LoggerContext { + buffer: string = ""; + #_functions = new Map(); + + constructor() { + for(let level of allLogLevels) { + this.#_functions.set(level, (message: string, messageType?: string) => { + if (messageType) { + this.buffer += `[${messageType}]`; + } + this.buffer += `[${LogLevel[level]}] ${message}\n`; + }); + } + } + + getFunction(level: LogLevel) { + return this.#_functions.get(level); + } + +}; + +/*@devdoc + * The Logger class serves as a convenience utility and a centralized configuration point for logging. + * + * @class Logger + */ +export class Logger +{ + #_context: LoggerContext; + #_activeFunctions = new Map(); + #_typeFilter?: Array; + + #_messageIds = new Map(); + #_typedMessageIds = new Map(); + #_messageFlags = new Map(); + + /*@devdoc + * Creates a logger instance with a specified context. + * @param {context} LoggerContext - The context this instance will use for output, + * which can also store any addition state, like an output buffer. + */ + constructor(context: LoggerContext) { + this.#_context = context; + this.filterLevels(() => true); + } + + /*@devdoc + * Logs a message at an appropriate level, with optional type. + * @param {level} LogLevel - the level to log at + * @param {message} string - the message to log + * @param {messageType} string - optional user defined message type + */ + level(level: LogLevel, message: string, messageType?: string) { + const log = this.#_activeFunctions.get(level); + if (log && this.#_isTypeAllowed(messageType) ) { + log(message, messageType); + } + } + + /*@devdoc + * Logs a message at an appropriate level, with optional type, only once. + * @param {level} LogLevel - the level to log at + * @param {message} string - the message to log + * @param {messageType} string - optional user defined message type + */ + once(level: LogLevel, message: string, messageType?: string) { + const idMap = messageType ? this.#_typedMessageIds : this.#_messageIds; + + const key = `${messageType} | ${level} | ${message}`; + + let id: Object; + + if(!idMap.has(key)) { + id = new Object(); + idMap.set(key, id); + } else{ + id = idMap.get(key) as Object; + } + + if (!this.#_messageFlags.get(id)) { + this.#_messageFlags.set(id, true); + this.level(level, message, messageType); + } + } + + /*@devdoc + * Logs a message at the default level, with optional type. + * @param {message} string - the message to log + * @param {messageType} string - optional user defined message type + */ + default(message: string, messageType?: string) { + this.level(LogLevel.DEFAULT, message, messageType); + } + + /*@devdoc + * Logs a message at the info level, with optional type. + * @param {message} string - the message to log + * @param {messageType} string - optional user defined message type + */ + info(message: string, messageType?: string) { + this.level(LogLevel.INFO, message, messageType); + } + + /*@devdoc + * Logs a message at the debug level, with optional type. + * @param {message} string - the message to log + * @param {messageType} string - optional user defined message type + */ + debug(message: string, messageType?: string) { + this.level(LogLevel.DEBUG, message, messageType); + } + + /*@devdoc + * Logs a message at the warning level, with optional type. + * @param {message} string - the message to log + * @param {messageType} string - optional user defined message type + */ + warning(message: string, messageType?: string) { + this.level(LogLevel.WARNING, message, messageType); + } + + /*@devdoc + * Logs a message at the error level, with optional type. + * @param {message} string - the message to log + * @param {messageType} string - optional user defined message type + */ + error(message: string, messageType?: string) { + this.level(LogLevel.ERROR, message, messageType); + } + + /*@devdoc + * Applies a filer to all subsequently logged messages, based on level. + * @param {pred} (level: LogLevel) => boolean - the filtering condition + */ + filterLevels(pred: (level: LogLevel) => boolean) { + this.#_activeFunctions = new Map(); + for (var level of allLogLevels) { + const func = this.#_context.getFunction(level); + if (func && pred(level)) { + this.#_activeFunctions.set(level, func); + } + } + } + + /*@devdoc + * Sets a filter for all subsequently logged messages, based on user defined types. + * @param {filter} Array - the collection of types to allow, + * presence or presence of undefined value will determine whether to show or hide messages with no type specified. + */ + setTypeFilter(filter?: Array) { + this.#_typeFilter = filter; + } + + #_isTypeAllowed(messageType?: string) { + return !this.#_typeFilter || this.#_typeFilter.includes(messageType); + } + +} + +/*@devdoc + * The Log is the main Logger instance used in the SDK. + */ +const Log = new Logger(new ConsoleLoggerContext()); + +export default Log; diff --git a/tests/Log.unit.test.ts b/tests/Log.unit.test.ts new file mode 100644 index 00000000..2fb09bfa --- /dev/null +++ b/tests/Log.unit.test.ts @@ -0,0 +1,269 @@ +// +// DomainServer.unit.test.js +// +// Created by Nshan G. on 30 Oct 2021. +// Copyright 2021 Vircadia contributors. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import {Logger, LogLevel, ConsoleLoggerContext, StringLoggerContext} from "../src/domain/shared/Log"; + +describe("Logger - unit tests", () => { + + test("Console context", () => { + + const logger = new Logger(new ConsoleLoggerContext()); + + const debug = jest.spyOn(console, "debug").mockImplementation(() => { /* no-op */ }); + const log = jest.spyOn(console, "log").mockImplementation(() => { /* no-op */ }); + const info = jest.spyOn(console, "info").mockImplementation(() => { /* no-op */ }); + const warn = jest.spyOn(console, "warn").mockImplementation(() => { /* no-op */ }); + const error = jest.spyOn(console, "error").mockImplementation(() => { /* no-op */ }); + + logger.debug("Console debug message"); + logger.default("Console log message"); + logger.info("Console info message"); + logger.warning("Console warning message"); + logger.error("Console error message"); + + expect(debug).toBeCalledWith("Console debug message"); + expect(log).toBeCalledWith("Console log message"); + expect(info).toBeCalledWith("Console info message"); + expect(warn).toBeCalledWith("Console warning message"); + expect(error).toBeCalledWith("Console error message"); + + debug.mockClear(); + log.mockClear(); + info.mockClear(); + warn.mockClear(); + error.mockClear(); + + logger.debug("Console debug message", "Type 1"); + logger.default("Console log message", "Type 2"); + logger.info("Console info message", "Type 3"); + logger.warning("Console warning message", "Type 4"); + logger.error("Console error message", "Type 5"); + + expect(debug).toBeCalledWith("[Type 1]", "Console debug message"); + expect(log).toBeCalledWith("[Type 2]", "Console log message"); + expect(info).toBeCalledWith("[Type 3]", "Console info message"); + expect(warn).toBeCalledWith("[Type 4]", "Console warning message"); + expect(error).toBeCalledWith("[Type 5]", "Console error message"); + + debug.mockRestore(); + log.mockRestore(); + info.mockRestore(); + warn.mockRestore(); + error.mockRestore(); + + }); + + test("String context", () => { + const context = new StringLoggerContext(); + const logger = new Logger(context); + + logger.debug("Debug message"); + logger.default("Default message"); + logger.info("Info message"); + logger.warning("Warning message"); + logger.error("Error message"); + + expect(context.buffer).toBe("" + + "[DEBUG] Debug message\n" + + "[DEFAULT] Default message\n" + + "[INFO] Info message\n" + + "[WARNING] Warning message\n" + + "[ERROR] Error message\n" + ); + context.buffer = ""; + + logger.debug("Debug message", "Type 1"); + logger.default("Default message", "Type 2"); + logger.info("Info message", "Type 3"); + logger.warning("Warning message", "Type 4"); + logger.error("Error message", "Type 5"); + + expect(context.buffer).toBe("" + + "[Type 1][DEBUG] Debug message\n" + + "[Type 2][DEFAULT] Default message\n" + + "[Type 3][INFO] Info message\n" + + "[Type 4][WARNING] Warning message\n" + + "[Type 5][ERROR] Error message\n" + ); + }); + + test("Filtering by level", () => { + const context = new StringLoggerContext(); + const logger = new Logger(context); + + logger.filterLevels(level => level >= LogLevel.INFO); + + logger.debug("Debug message"); + logger.default("Default message"); + logger.info("Info message"); + logger.warning("Warning message"); + logger.error("Error message"); + + expect(context.buffer).toBe("" + + "[INFO] Info message\n" + + "[WARNING] Warning message\n" + + "[ERROR] Error message\n" + ); + context.buffer = ""; + + logger.filterLevels(level => level <= LogLevel.DEFAULT); + + logger.debug("Debug message"); + logger.default("Default message"); + logger.info("Info message"); + logger.warning("Warning message"); + logger.error("Error message"); + + expect(context.buffer).toBe("" + + "[DEBUG] Debug message\n" + + "[DEFAULT] Default message\n" + ); + context.buffer = ""; + + logger.filterLevels(level => [LogLevel.DEBUG, LogLevel.ERROR].includes(level)); + + logger.debug("Debug message"); + logger.default("Default message"); + logger.info("Info message"); + logger.warning("Warning message"); + logger.error("Error message"); + + expect(context.buffer).toBe("" + + "[DEBUG] Debug message\n" + + "[ERROR] Error message\n" + ); + + }); + + test("Filtering by type", () => { + const context = new StringLoggerContext(); + const logger = new Logger(context); + + logger.setTypeFilter(["Type 2", "Type 4"]); + + logger.default("message"); + logger.default("message 1", "Type 1"); + logger.default("message 2", "Type 2"); + logger.default("message 3", "Type 3"); + logger.default("message 4", "Type 4"); + logger.default("message 5", "Type 5"); + + expect(context.buffer).toBe("" + + "[Type 2][DEFAULT] message 2\n" + + "[Type 4][DEFAULT] message 4\n" + ); + context.buffer = ""; + + logger.setTypeFilter(["Type 1", "Type 3", undefined]); + + logger.default("message"); + logger.default("message 1", "Type 1"); + logger.default("message 2", "Type 2"); + logger.default("message 3", "Type 3"); + logger.default("message 4", "Type 4"); + logger.default("message 5", "Type 5"); + + expect(context.buffer).toBe("" + + "[DEFAULT] message\n" + + "[Type 1][DEFAULT] message 1\n" + + "[Type 3][DEFAULT] message 3\n" + ); + context.buffer = ""; + + logger.setTypeFilter([undefined]); + + logger.default("message"); + logger.default("message 1", "Type 1"); + logger.default("message 2", "Type 2"); + logger.default("message 3", "Type 3"); + logger.default("message 4", "Type 4"); + logger.default("message 5", "Type 5"); + + expect(context.buffer).toBe("" + + "[DEFAULT] message\n" + ); + context.buffer = ""; + + logger.setTypeFilter(); + + logger.default("message"); + logger.default("message 1", "Type 1"); + logger.default("message 2", "Type 2"); + logger.default("message 3", "Type 3"); + logger.default("message 4", "Type 4"); + logger.default("message 5", "Type 5"); + + expect(context.buffer).toBe("" + + "[DEFAULT] message\n" + + "[Type 1][DEFAULT] message 1\n" + + "[Type 2][DEFAULT] message 2\n" + + "[Type 3][DEFAULT] message 3\n" + + "[Type 4][DEFAULT] message 4\n" + + "[Type 5][DEFAULT] message 5\n" + ); + context.buffer = ""; + + }); + + test("Mixed filtering", () => { + const context = new StringLoggerContext(); + const logger = new Logger(context); + + logger.filterLevels(level => level <= LogLevel.DEFAULT); + logger.setTypeFilter(["Type 2"]); + + logger.default("message"); + logger.debug("Debug message 1", "Type 1"); + logger.default("Default message 2", "Type 2"); + logger.info("Info message 1", "Type 1"); + logger.warning("message 2", "Type 2"); + logger.error("message 1", "Type 1"); + + expect(context.buffer).toBe("" + + "[Type 2][DEFAULT] Default message 2\n" + ); + context.buffer = ""; + }); + + test("Logger.once", () => { + const context = new StringLoggerContext(); + const logger = new Logger(context); + + logger.once(LogLevel.DEBUG, "message"); + logger.once(LogLevel.DEBUG, "message"); + logger.once(LogLevel.DEBUG, "message"); + + logger.once(LogLevel.DEBUG, "message", "Type"); + logger.once(LogLevel.DEBUG, "message", "Type"); + logger.once(LogLevel.DEBUG, "message", "Type"); + + logger.once(LogLevel.DEBUG, "message", "undefined"); + logger.once(LogLevel.DEBUG, "message", "undefined"); + logger.once(LogLevel.DEBUG, "message", "undefined"); + + logger.once(LogLevel.INFO, "message"); + logger.once(LogLevel.INFO, "message"); + logger.once(LogLevel.INFO, "message"); + + logger.once(LogLevel.DEBUG, "another message"); + logger.once(LogLevel.DEBUG, "another message"); + logger.once(LogLevel.DEBUG, "another message"); + + expect(context.buffer).toBe("" + + "[DEBUG] message\n" + + "[Type][DEBUG] message\n" + + "[undefined][DEBUG] message\n" + + "[INFO] message\n" + + "[DEBUG] another message\n" + ); + context.buffer = ""; + }); + +}); From eed3499fa416e7665727efe8672b1199d97fb2e1 Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 30 Oct 2021 23:40:16 +0400 Subject: [PATCH 02/23] Some not implemented warnings logged only once. --- src/domain/audio-client/AudioClient.ts | 5 +++-- src/domain/audio/InboundAudioStream.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/domain/audio-client/AudioClient.ts b/src/domain/audio-client/AudioClient.ts index 2dfafc63..c3e084cc 100644 --- a/src/domain/audio-client/AudioClient.ts +++ b/src/domain/audio-client/AudioClient.ts @@ -22,6 +22,7 @@ import PacketScribe from "../networking/packets/PacketScribe"; import PacketType, { PacketTypeValue } from "../networking/udt/PacketHeaders"; import assert from "../shared/assert"; import ContextManager from "../shared/ContextManager"; +import Log, {LogLevel} from "../shared/Log"; /*@devdoc @@ -405,7 +406,7 @@ class AudioClient { #processStreamStatsPacket = (message: ReceivedMessage, sendingNode?: Node): void => { // eslint-disable-line // C++ void AudioIOStats::processStreamStatsPacket(ReceivedMessage*, Node* sendingNode) - console.warn("AudioClient: AudioStreamStats packet not processed."); + Log.once(LogLevel.WARNING, "AudioClient: AudioStreamStats packet not processed."); // WEBRTC TODO: Address further C++ code. @@ -417,7 +418,7 @@ class AudioClient { #handleAudioEnvironmentDataPacket = (message: ReceivedMessage): void => { // eslint-disable-line // C++ void handleAudioEnvironmentDataPacket(ReceivedMessage* message) - console.warn("AudioClient: AudioEnvironment packet not processed."); + Log.once(LogLevel.WARNING, "AudioClient: AudioEnvironment packet not processed."); // WEBRTC TODO: Address further C++ code. diff --git a/src/domain/audio/InboundAudioStream.ts b/src/domain/audio/InboundAudioStream.ts index 51f9e20b..0e29571b 100644 --- a/src/domain/audio/InboundAudioStream.ts +++ b/src/domain/audio/InboundAudioStream.ts @@ -16,6 +16,7 @@ import { SilentAudioFrameDetails } from "../networking/packets/SilentAudioFrame" import PacketType from "../networking/udt/PacketHeaders"; import UDT from "../networking/udt/UDT"; import ContextManager from "../shared/ContextManager"; +import Log, {LogLevel} from "../shared/Log"; /*@devdoc @@ -124,7 +125,7 @@ class InboundAudioStream { // C++ int writeDroppableSilentFrames(int silentFrames) // WEBRTC TODO: Address further C++ code. - console.warn("InboundAudioStream.#writeDroppableSilentFrames() not implemented. Frames:", silentFrames); + Log.once(LogLevel.WARNING, `InboundAudioStream.#writeDroppableSilentFrames() not implemented. Frames: ${silentFrames}`); } From 2684c9f49d5f3e11476325c273838e4dad742270 Mon Sep 17 00:00:00 2001 From: namark Date: Sun, 31 Oct 2021 00:36:24 +0400 Subject: [PATCH 03/23] Fixed linting errors. --- src/domain/audio-client/AudioClient.ts | 2 +- src/domain/audio/InboundAudioStream.ts | 2 +- src/domain/shared/Log.ts | 90 +++++++++++++----------- tests/Log.unit.test.ts | 96 +++++++++++++------------- 4 files changed, 100 insertions(+), 90 deletions(-) diff --git a/src/domain/audio-client/AudioClient.ts b/src/domain/audio-client/AudioClient.ts index c3e084cc..767e2c86 100644 --- a/src/domain/audio-client/AudioClient.ts +++ b/src/domain/audio-client/AudioClient.ts @@ -22,7 +22,7 @@ import PacketScribe from "../networking/packets/PacketScribe"; import PacketType, { PacketTypeValue } from "../networking/udt/PacketHeaders"; import assert from "../shared/assert"; import ContextManager from "../shared/ContextManager"; -import Log, {LogLevel} from "../shared/Log"; +import Log, { LogLevel } from "../shared/Log"; /*@devdoc diff --git a/src/domain/audio/InboundAudioStream.ts b/src/domain/audio/InboundAudioStream.ts index 0e29571b..94d5a549 100644 --- a/src/domain/audio/InboundAudioStream.ts +++ b/src/domain/audio/InboundAudioStream.ts @@ -16,7 +16,7 @@ import { SilentAudioFrameDetails } from "../networking/packets/SilentAudioFrame" import PacketType from "../networking/udt/PacketHeaders"; import UDT from "../networking/udt/UDT"; import ContextManager from "../shared/ContextManager"; -import Log, {LogLevel} from "../shared/Log"; +import Log, { LogLevel } from "../shared/Log"; /*@devdoc diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index 58affd77..ef7a3c50 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -14,7 +14,7 @@ export enum LogLevel { DEFAULT, INFO, WARNING, - ERROR, + ERROR } export const allLogLevels = [ @@ -22,7 +22,7 @@ export const allLogLevels = [ LogLevel.DEFAULT, LogLevel.INFO, LogLevel.WARNING, - LogLevel.ERROR, + LogLevel.ERROR ] as const; type LogFunction = (message: string, messageType?: string) => void; @@ -44,23 +44,32 @@ export interface LoggerContext { */ export class ConsoleLoggerContext implements LoggerContext { - static #_typeFirst(func: (fisrs:string, second?:string) => void) { - return (message: string, messageType?: string) => messageType ? func(`[${messageType}]`, message) : func(message); ; + static #_typeFirst(func: (fisrs: string, second?: string) => void): LogFunction { + return (message: string, messageType?: string) => { + if (messageType) { + return func(`[${messageType}]`, message); + } + return func(message); + }; } + // it is necessary to capture the console object here, for things like jest's mocks to work, // hence the no-op looking lambda wrappers static #_functions = new Map([ + /* eslint-disable @typescript-eslint/no-explicit-any */ [LogLevel.DEFAULT, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.log(...params))], [LogLevel.DEBUG, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.debug(...params))], [LogLevel.INFO, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.info(...params))], [LogLevel.WARNING, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.warn(...params))], [LogLevel.ERROR, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.error(...params))] + /* eslint-enable @typescript-eslint/no-explicit-any */ ]); - getFunction(level: LogLevel) - { + /* eslint-disable class-methods-use-this */ + getFunction(level: LogLevel): LogFunction | undefined { return ConsoleLoggerContext.#_functions.get(level); } + /* eslint-enable class-methods-use-this */ } /*@devdoc @@ -69,40 +78,39 @@ export class ConsoleLoggerContext implements LoggerContext { * @class Logger */ export class StringLoggerContext implements LoggerContext { - buffer: string = ""; + buffer = ""; #_functions = new Map(); constructor() { - for(let level of allLogLevels) { + for (const level of allLogLevels) { this.#_functions.set(level, (message: string, messageType?: string) => { if (messageType) { - this.buffer += `[${messageType}]`; + this.buffer += `[${messageType}]`; } - this.buffer += `[${LogLevel[level]}] ${message}\n`; + this.buffer += `[${LogLevel[level] as string}] ${message}\n`; }); } } - getFunction(level: LogLevel) { + getFunction(level: LogLevel): LogFunction | undefined { return this.#_functions.get(level); } -}; +} /*@devdoc * The Logger class serves as a convenience utility and a centralized configuration point for logging. * * @class Logger */ -export class Logger -{ +export class Logger { #_context: LoggerContext; - #_activeFunctions = new Map(); + #_activeFunctions = new Map(); #_typeFilter?: Array; - #_messageIds = new Map(); - #_typedMessageIds = new Map(); - #_messageFlags = new Map(); + #_messageIds = new Map(); + #_typedMessageIds = new Map(); + #_messageFlags = new Map(); /*@devdoc * Creates a logger instance with a specified context. @@ -120,9 +128,9 @@ export class Logger * @param {message} string - the message to log * @param {messageType} string - optional user defined message type */ - level(level: LogLevel, message: string, messageType?: string) { + level(level: LogLevel, message: string, messageType?: string): void { const log = this.#_activeFunctions.get(level); - if (log && this.#_isTypeAllowed(messageType) ) { + if (log && this.#_isTypeAllowed(messageType)) { log(message, messageType); } } @@ -133,19 +141,12 @@ export class Logger * @param {message} string - the message to log * @param {messageType} string - optional user defined message type */ - once(level: LogLevel, message: string, messageType?: string) { + once(level: LogLevel, message: string, messageType?: string): void { const idMap = messageType ? this.#_typedMessageIds : this.#_messageIds; - const key = `${messageType} | ${level} | ${message}`; + const key = `${messageType || "undefined"} | ${level} | ${message}`; - let id: Object; - - if(!idMap.has(key)) { - id = new Object(); - idMap.set(key, id); - } else{ - id = idMap.get(key) as Object; - } + const id = Logger.#_getMessageId(idMap, key); if (!this.#_messageFlags.get(id)) { this.#_messageFlags.set(id, true); @@ -158,7 +159,7 @@ export class Logger * @param {message} string - the message to log * @param {messageType} string - optional user defined message type */ - default(message: string, messageType?: string) { + message(message: string, messageType?: string): void { this.level(LogLevel.DEFAULT, message, messageType); } @@ -167,7 +168,7 @@ export class Logger * @param {message} string - the message to log * @param {messageType} string - optional user defined message type */ - info(message: string, messageType?: string) { + info(message: string, messageType?: string): void { this.level(LogLevel.INFO, message, messageType); } @@ -176,7 +177,7 @@ export class Logger * @param {message} string - the message to log * @param {messageType} string - optional user defined message type */ - debug(message: string, messageType?: string) { + debug(message: string, messageType?: string): void { this.level(LogLevel.DEBUG, message, messageType); } @@ -185,7 +186,7 @@ export class Logger * @param {message} string - the message to log * @param {messageType} string - optional user defined message type */ - warning(message: string, messageType?: string) { + warning(message: string, messageType?: string): void { this.level(LogLevel.WARNING, message, messageType); } @@ -194,7 +195,7 @@ export class Logger * @param {message} string - the message to log * @param {messageType} string - optional user defined message type */ - error(message: string, messageType?: string) { + error(message: string, messageType?: string): void { this.level(LogLevel.ERROR, message, messageType); } @@ -202,12 +203,12 @@ export class Logger * Applies a filer to all subsequently logged messages, based on level. * @param {pred} (level: LogLevel) => boolean - the filtering condition */ - filterLevels(pred: (level: LogLevel) => boolean) { + filterLevels(pred: (level: LogLevel) => boolean): void { this.#_activeFunctions = new Map(); - for (var level of allLogLevels) { + for (const level of allLogLevels) { const func = this.#_context.getFunction(level); if (func && pred(level)) { - this.#_activeFunctions.set(level, func); + this.#_activeFunctions.set(level, func); } } } @@ -217,14 +218,23 @@ export class Logger * @param {filter} Array - the collection of types to allow, * presence or presence of undefined value will determine whether to show or hide messages with no type specified. */ - setTypeFilter(filter?: Array) { + setTypeFilter(filter?: Array): void { this.#_typeFilter = filter; } - #_isTypeAllowed(messageType?: string) { + #_isTypeAllowed(messageType?: string): boolean { return !this.#_typeFilter || this.#_typeFilter.includes(messageType); } + static #_getMessageId(idMap: Map, key: string): unknown { + if (!idMap.has(key)) { + const id = {}; + idMap.set(key, id); + return id; + } + return idMap.get(key); + } + } /*@devdoc diff --git a/tests/Log.unit.test.ts b/tests/Log.unit.test.ts index 2fb09bfa..090b2c36 100644 --- a/tests/Log.unit.test.ts +++ b/tests/Log.unit.test.ts @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import {Logger, LogLevel, ConsoleLoggerContext, StringLoggerContext} from "../src/domain/shared/Log"; +import { Logger, LogLevel, ConsoleLoggerContext, StringLoggerContext } from "../src/domain/shared/Log"; describe("Logger - unit tests", () => { @@ -23,16 +23,16 @@ describe("Logger - unit tests", () => { const error = jest.spyOn(console, "error").mockImplementation(() => { /* no-op */ }); logger.debug("Console debug message"); - logger.default("Console log message"); + logger.message("Console log message"); logger.info("Console info message"); logger.warning("Console warning message"); logger.error("Console error message"); - expect(debug).toBeCalledWith("Console debug message"); - expect(log).toBeCalledWith("Console log message"); - expect(info).toBeCalledWith("Console info message"); - expect(warn).toBeCalledWith("Console warning message"); - expect(error).toBeCalledWith("Console error message"); + expect(debug).toHaveBeenCalledWith("Console debug message"); + expect(log).toHaveBeenCalledWith("Console log message"); + expect(info).toHaveBeenCalledWith("Console info message"); + expect(warn).toHaveBeenCalledWith("Console warning message"); + expect(error).toHaveBeenCalledWith("Console error message"); debug.mockClear(); log.mockClear(); @@ -41,16 +41,16 @@ describe("Logger - unit tests", () => { error.mockClear(); logger.debug("Console debug message", "Type 1"); - logger.default("Console log message", "Type 2"); + logger.message("Console log message", "Type 2"); logger.info("Console info message", "Type 3"); logger.warning("Console warning message", "Type 4"); logger.error("Console error message", "Type 5"); - expect(debug).toBeCalledWith("[Type 1]", "Console debug message"); - expect(log).toBeCalledWith("[Type 2]", "Console log message"); - expect(info).toBeCalledWith("[Type 3]", "Console info message"); - expect(warn).toBeCalledWith("[Type 4]", "Console warning message"); - expect(error).toBeCalledWith("[Type 5]", "Console error message"); + expect(debug).toHaveBeenCalledWith("[Type 1]", "Console debug message"); + expect(log).toHaveBeenCalledWith("[Type 2]", "Console log message"); + expect(info).toHaveBeenCalledWith("[Type 3]", "Console info message"); + expect(warn).toHaveBeenCalledWith("[Type 4]", "Console warning message"); + expect(error).toHaveBeenCalledWith("[Type 5]", "Console error message"); debug.mockRestore(); log.mockRestore(); @@ -65,7 +65,7 @@ describe("Logger - unit tests", () => { const logger = new Logger(context); logger.debug("Debug message"); - logger.default("Default message"); + logger.message("Default message"); logger.info("Info message"); logger.warning("Warning message"); logger.error("Error message"); @@ -80,7 +80,7 @@ describe("Logger - unit tests", () => { context.buffer = ""; logger.debug("Debug message", "Type 1"); - logger.default("Default message", "Type 2"); + logger.message("Default message", "Type 2"); logger.info("Info message", "Type 3"); logger.warning("Warning message", "Type 4"); logger.error("Error message", "Type 5"); @@ -98,10 +98,10 @@ describe("Logger - unit tests", () => { const context = new StringLoggerContext(); const logger = new Logger(context); - logger.filterLevels(level => level >= LogLevel.INFO); + logger.filterLevels((level) => level >= LogLevel.INFO); logger.debug("Debug message"); - logger.default("Default message"); + logger.message("Default message"); logger.info("Info message"); logger.warning("Warning message"); logger.error("Error message"); @@ -113,10 +113,10 @@ describe("Logger - unit tests", () => { ); context.buffer = ""; - logger.filterLevels(level => level <= LogLevel.DEFAULT); + logger.filterLevels((level) => level <= LogLevel.DEFAULT); logger.debug("Debug message"); - logger.default("Default message"); + logger.message("Default message"); logger.info("Info message"); logger.warning("Warning message"); logger.error("Error message"); @@ -127,10 +127,10 @@ describe("Logger - unit tests", () => { ); context.buffer = ""; - logger.filterLevels(level => [LogLevel.DEBUG, LogLevel.ERROR].includes(level)); + logger.filterLevels((level) => [LogLevel.DEBUG, LogLevel.ERROR].includes(level)); logger.debug("Debug message"); - logger.default("Default message"); + logger.message("Default message"); logger.info("Info message"); logger.warning("Warning message"); logger.error("Error message"); @@ -148,12 +148,12 @@ describe("Logger - unit tests", () => { logger.setTypeFilter(["Type 2", "Type 4"]); - logger.default("message"); - logger.default("message 1", "Type 1"); - logger.default("message 2", "Type 2"); - logger.default("message 3", "Type 3"); - logger.default("message 4", "Type 4"); - logger.default("message 5", "Type 5"); + logger.message("message"); + logger.message("message 1", "Type 1"); + logger.message("message 2", "Type 2"); + logger.message("message 3", "Type 3"); + logger.message("message 4", "Type 4"); + logger.message("message 5", "Type 5"); expect(context.buffer).toBe("" + "[Type 2][DEFAULT] message 2\n" @@ -163,12 +163,12 @@ describe("Logger - unit tests", () => { logger.setTypeFilter(["Type 1", "Type 3", undefined]); - logger.default("message"); - logger.default("message 1", "Type 1"); - logger.default("message 2", "Type 2"); - logger.default("message 3", "Type 3"); - logger.default("message 4", "Type 4"); - logger.default("message 5", "Type 5"); + logger.message("message"); + logger.message("message 1", "Type 1"); + logger.message("message 2", "Type 2"); + logger.message("message 3", "Type 3"); + logger.message("message 4", "Type 4"); + logger.message("message 5", "Type 5"); expect(context.buffer).toBe("" + "[DEFAULT] message\n" @@ -179,12 +179,12 @@ describe("Logger - unit tests", () => { logger.setTypeFilter([undefined]); - logger.default("message"); - logger.default("message 1", "Type 1"); - logger.default("message 2", "Type 2"); - logger.default("message 3", "Type 3"); - logger.default("message 4", "Type 4"); - logger.default("message 5", "Type 5"); + logger.message("message"); + logger.message("message 1", "Type 1"); + logger.message("message 2", "Type 2"); + logger.message("message 3", "Type 3"); + logger.message("message 4", "Type 4"); + logger.message("message 5", "Type 5"); expect(context.buffer).toBe("" + "[DEFAULT] message\n" @@ -193,12 +193,12 @@ describe("Logger - unit tests", () => { logger.setTypeFilter(); - logger.default("message"); - logger.default("message 1", "Type 1"); - logger.default("message 2", "Type 2"); - logger.default("message 3", "Type 3"); - logger.default("message 4", "Type 4"); - logger.default("message 5", "Type 5"); + logger.message("message"); + logger.message("message 1", "Type 1"); + logger.message("message 2", "Type 2"); + logger.message("message 3", "Type 3"); + logger.message("message 4", "Type 4"); + logger.message("message 5", "Type 5"); expect(context.buffer).toBe("" + "[DEFAULT] message\n" @@ -216,12 +216,12 @@ describe("Logger - unit tests", () => { const context = new StringLoggerContext(); const logger = new Logger(context); - logger.filterLevels(level => level <= LogLevel.DEFAULT); + logger.filterLevels((level) => level <= LogLevel.DEFAULT); logger.setTypeFilter(["Type 2"]); - logger.default("message"); + logger.message("message"); logger.debug("Debug message 1", "Type 1"); - logger.default("Default message 2", "Type 2"); + logger.message("Default message 2", "Type 2"); logger.info("Info message 1", "Type 1"); logger.warning("message 2", "Type 2"); logger.error("message 1", "Type 1"); From ceddde4375fcc5fcda5090097ead77ffa922fe51 Mon Sep 17 00:00:00 2001 From: namark Date: Sun, 31 Oct 2021 02:08:04 +0400 Subject: [PATCH 04/23] Moved logger test to appropriate directory. --- tests/{ => domain/shared}/Log.unit.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{ => domain/shared}/Log.unit.test.ts (100%) diff --git a/tests/Log.unit.test.ts b/tests/domain/shared/Log.unit.test.ts similarity index 100% rename from tests/Log.unit.test.ts rename to tests/domain/shared/Log.unit.test.ts From 7fd356fff44be4658afe4dc01f9537d9b1812ecf Mon Sep 17 00:00:00 2001 From: namark Date: Sun, 31 Oct 2021 02:10:38 +0400 Subject: [PATCH 05/23] Fixed logger unit test import. --- tests/domain/shared/Log.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/domain/shared/Log.unit.test.ts b/tests/domain/shared/Log.unit.test.ts index 090b2c36..f4196b2e 100644 --- a/tests/domain/shared/Log.unit.test.ts +++ b/tests/domain/shared/Log.unit.test.ts @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import { Logger, LogLevel, ConsoleLoggerContext, StringLoggerContext } from "../src/domain/shared/Log"; +import { Logger, LogLevel, ConsoleLoggerContext, StringLoggerContext } from "../../../src/domain/shared/Log"; describe("Logger - unit tests", () => { From da62e8486c7ef3db57e970a206c908566a463d7a Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 6 Nov 2021 02:50:21 +0400 Subject: [PATCH 06/23] Fixed new linting errors in logger code. --- src/domain/shared/Log.ts | 34 +++++++++++++++++++++++----- tests/domain/shared/Log.unit.test.ts | 16 +++++++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index ef7a3c50..242d396a 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -57,11 +57,31 @@ export class ConsoleLoggerContext implements LoggerContext { // hence the no-op looking lambda wrappers static #_functions = new Map([ /* eslint-disable @typescript-eslint/no-explicit-any */ - [LogLevel.DEFAULT, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.log(...params))], - [LogLevel.DEBUG, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.debug(...params))], - [LogLevel.INFO, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.info(...params))], - [LogLevel.WARNING, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.warn(...params))], - [LogLevel.ERROR, ConsoleLoggerContext.#_typeFirst((...params: any[]) => console.error(...params))] + [ + LogLevel.DEFAULT, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { + return console.log(...params); + }) + ], + [ + LogLevel.DEBUG, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { + return console.debug(...params); + }) + ], + [ + LogLevel.INFO, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { + return console.info(...params); + }) + ], + [ + LogLevel.WARNING, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { + return console.warn(...params); + }) + ], + [ + LogLevel.ERROR, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { + return console.error(...params); + }) + ] /* eslint-enable @typescript-eslint/no-explicit-any */ ]); @@ -119,7 +139,9 @@ export class Logger { */ constructor(context: LoggerContext) { this.#_context = context; - this.filterLevels(() => true); + this.filterLevels(() => { + return true; + }); } /*@devdoc diff --git a/tests/domain/shared/Log.unit.test.ts b/tests/domain/shared/Log.unit.test.ts index f4196b2e..2b4dbb75 100644 --- a/tests/domain/shared/Log.unit.test.ts +++ b/tests/domain/shared/Log.unit.test.ts @@ -98,7 +98,9 @@ describe("Logger - unit tests", () => { const context = new StringLoggerContext(); const logger = new Logger(context); - logger.filterLevels((level) => level >= LogLevel.INFO); + logger.filterLevels((level) => { + return level >= LogLevel.INFO; + }); logger.debug("Debug message"); logger.message("Default message"); @@ -113,7 +115,9 @@ describe("Logger - unit tests", () => { ); context.buffer = ""; - logger.filterLevels((level) => level <= LogLevel.DEFAULT); + logger.filterLevels((level) => { + return level <= LogLevel.DEFAULT; + }); logger.debug("Debug message"); logger.message("Default message"); @@ -127,7 +131,9 @@ describe("Logger - unit tests", () => { ); context.buffer = ""; - logger.filterLevels((level) => [LogLevel.DEBUG, LogLevel.ERROR].includes(level)); + logger.filterLevels((level) => { + return [LogLevel.DEBUG, LogLevel.ERROR].includes(level); + }); logger.debug("Debug message"); logger.message("Default message"); @@ -216,7 +222,9 @@ describe("Logger - unit tests", () => { const context = new StringLoggerContext(); const logger = new Logger(context); - logger.filterLevels((level) => level <= LogLevel.DEFAULT); + logger.filterLevels((level) => { + return level <= LogLevel.DEFAULT; + }); logger.setTypeFilter(["Type 2"]); logger.message("message"); From d9e9462303e47727654fe8684f8fcb2a562af581 Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 13 Nov 2021 16:41:43 +0400 Subject: [PATCH 07/23] Minor typos in centralized logger. --- src/domain/shared/Log.ts | 2 +- tests/domain/shared/Log.unit.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index 242d396a..56a849f2 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -44,7 +44,7 @@ export interface LoggerContext { */ export class ConsoleLoggerContext implements LoggerContext { - static #_typeFirst(func: (fisrs: string, second?: string) => void): LogFunction { + static #_typeFirst(func: (first: string, second?: string) => void): LogFunction { return (message: string, messageType?: string) => { if (messageType) { return func(`[${messageType}]`, message); diff --git a/tests/domain/shared/Log.unit.test.ts b/tests/domain/shared/Log.unit.test.ts index 2b4dbb75..3d730cd1 100644 --- a/tests/domain/shared/Log.unit.test.ts +++ b/tests/domain/shared/Log.unit.test.ts @@ -1,5 +1,5 @@ // -// DomainServer.unit.test.js +// Log.unit.test.js // // Created by Nshan G. on 30 Oct 2021. // Copyright 2021 Vircadia contributors. From 76cefe66e6da13a5749bb50425b18804dc04863f Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 13 Nov 2021 17:03:22 +0400 Subject: [PATCH 08/23] Logging levels made available directly through Logger class. --- src/domain/audio-client/AudioClient.ts | 6 +++--- src/domain/shared/Log.ts | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/domain/audio-client/AudioClient.ts b/src/domain/audio-client/AudioClient.ts index 56fd7043..4c9f0e06 100644 --- a/src/domain/audio-client/AudioClient.ts +++ b/src/domain/audio-client/AudioClient.ts @@ -22,7 +22,7 @@ import PacketScribe from "../networking/packets/PacketScribe"; import PacketType, { PacketTypeValue } from "../networking/udt/PacketHeaders"; import assert from "../shared/assert"; import ContextManager from "../shared/ContextManager"; -import Log, { LogLevel } from "../shared/Log"; +import Log from "../shared/Log"; import Vec3, { vec3 } from "../shared/Vec3"; @@ -435,7 +435,7 @@ class AudioClient { #processStreamStatsPacket = (message: ReceivedMessage, sendingNode: Node | null): void => { // eslint-disable-line // C++ void AudioIOStats::processStreamStatsPacket(ReceivedMessage*, Node* sendingNode) - Log.once(LogLevel.WARNING, "AudioClient: AudioStreamStats packet not processed."); + Log.once(Log.WARNING, "AudioClient: AudioStreamStats packet not processed."); // WEBRTC TODO: Address further C++ code. @@ -447,7 +447,7 @@ class AudioClient { #handleAudioEnvironmentDataPacket = (message: ReceivedMessage): void => { // eslint-disable-line // C++ void handleAudioEnvironmentDataPacket(ReceivedMessage* message) - Log.once(LogLevel.WARNING, "AudioClient: AudioEnvironment packet not processed."); + Log.once(Log.WARNING, "AudioClient: AudioEnvironment packet not processed."); // WEBRTC TODO: Address further C++ code. diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index 56a849f2..b477d67f 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -124,6 +124,13 @@ export class StringLoggerContext implements LoggerContext { * @class Logger */ export class Logger { + + readonly DEBUG = LogLevel.DEBUG; + readonly DEFAULT = LogLevel.DEFAULT; + readonly INFO = LogLevel.INFO; + readonly WARNING = LogLevel.WARNING; + readonly ERROR = LogLevel.ERROR; + #_context: LoggerContext; #_activeFunctions = new Map(); #_typeFilter?: Array; From c7429e5e2ecd1b7b7f22bd19a6d7759f09bbc947 Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 13 Nov 2021 18:31:03 +0400 Subject: [PATCH 09/23] JSDoc fixes in centralized logger. --- src/domain/shared/Log.ts | 94 ++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 28 deletions(-) diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index b477d67f..39f572c8 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -9,6 +9,16 @@ // + +/*@devdoc + * The levels of the Logger class. + * @enum {number} + * @property {number} DEBUG + * @property {number} DEFAULT + * @property {number} INFO + * @property {number} WARNING + * @property {number} ERROR + */ export enum LogLevel { DEBUG, DEFAULT, @@ -17,6 +27,10 @@ export enum LogLevel { ERROR } +/*@devdoc + * An array containing all values of LogLevel enum in ascending order. + * @type {Array} + */ export const allLogLevels = [ LogLevel.DEBUG, LogLevel.DEFAULT, @@ -25,22 +39,32 @@ export const allLogLevels = [ LogLevel.ERROR ] as const; +/*@devdoc + * The log function type used by in LoggerContext interface. + * @callback LogFunction + * @param {string} message - The main body of the message. + * @param {string} [messageType] - The message type. + */ type LogFunction = (message: string, messageType?: string) => void; /*@devdoc * The LoggerContext is an interface for configuration of the output of the Logger class. * @interface LoggerContext - * */ export interface LoggerContext { + /*@devdoc + * Returns a function that the Logger class would use to log messages at a given level. + * @param {LogLevel} level - The level associated with the log function returned. + * @return {LogFunction | undefined} + */ getFunction(level: LogLevel): LogFunction | undefined; } - /*@devdoc - * The StringLoggerContext is a logger configuration for logging to the dev console. + * The ConsoleLoggerContext is a logger configuration for logging to the dev console. * - * @class Logger + * @class ConsoleLoggerContext + * @implements LoggerContext */ export class ConsoleLoggerContext implements LoggerContext { @@ -95,10 +119,14 @@ export class ConsoleLoggerContext implements LoggerContext { /*@devdoc * The StringLoggerContext is a configuration for logging to a string. * - * @class Logger + * @class StringLoggerContext + * @implements LoggerContext + * @property {string} buffer - The buffer to which all the log messages will be written. */ export class StringLoggerContext implements LoggerContext { + buffer = ""; + #_functions = new Map(); constructor() { @@ -122,6 +150,13 @@ export class StringLoggerContext implements LoggerContext { * The Logger class serves as a convenience utility and a centralized configuration point for logging. * * @class Logger + * @param {LoggerContext} context - The context this instance will use for output, + * which can also store any addition state, like an output buffer. + * @property {number} DEBUG - Alias for LogLevel.DEBUG. + * @property {number} DEFAULT - Alias for LogLevel.DEFAULT. + * @property {number} INFO - Alias for LogLevel.INFO. + * @property {number} WARNING - Alias for LogLevel.WARNING. + * @property {number} ERROR - Alias for LogLevel.ERROR. */ export class Logger { @@ -139,11 +174,6 @@ export class Logger { #_typedMessageIds = new Map(); #_messageFlags = new Map(); - /*@devdoc - * Creates a logger instance with a specified context. - * @param {context} LoggerContext - The context this instance will use for output, - * which can also store any addition state, like an output buffer. - */ constructor(context: LoggerContext) { this.#_context = context; this.filterLevels(() => { @@ -153,9 +183,9 @@ export class Logger { /*@devdoc * Logs a message at an appropriate level, with optional type. - * @param {level} LogLevel - the level to log at - * @param {message} string - the message to log - * @param {messageType} string - optional user defined message type + * @param {LogLevel} level - The level to log at. + * @param {string} message - The message to log. + * @param {string} [messageType] - User defined message type. */ level(level: LogLevel, message: string, messageType?: string): void { const log = this.#_activeFunctions.get(level); @@ -166,9 +196,9 @@ export class Logger { /*@devdoc * Logs a message at an appropriate level, with optional type, only once. - * @param {level} LogLevel - the level to log at - * @param {message} string - the message to log - * @param {messageType} string - optional user defined message type + * @param {LogLevel} level - The level to log at. + * @param {string} message - The message to log. + * @param {string} [messageType] - User defined message type. */ once(level: LogLevel, message: string, messageType?: string): void { const idMap = messageType ? this.#_typedMessageIds : this.#_messageIds; @@ -185,8 +215,8 @@ export class Logger { /*@devdoc * Logs a message at the default level, with optional type. - * @param {message} string - the message to log - * @param {messageType} string - optional user defined message type + * @param {string} message - The message to log. + * @param {string} [messageType] - User defined message type. */ message(message: string, messageType?: string): void { this.level(LogLevel.DEFAULT, message, messageType); @@ -194,8 +224,8 @@ export class Logger { /*@devdoc * Logs a message at the info level, with optional type. - * @param {message} string - the message to log - * @param {messageType} string - optional user defined message type + * @param {string} message - The message to log. + * @param {string} [messageType] - User defined message type. */ info(message: string, messageType?: string): void { this.level(LogLevel.INFO, message, messageType); @@ -203,8 +233,8 @@ export class Logger { /*@devdoc * Logs a message at the debug level, with optional type. - * @param {message} string - the message to log - * @param {messageType} string - optional user defined message type + * @param {string} message - The message to log. + * @param {string} [messageType] - User defined message type. */ debug(message: string, messageType?: string): void { this.level(LogLevel.DEBUG, message, messageType); @@ -212,8 +242,8 @@ export class Logger { /*@devdoc * Logs a message at the warning level, with optional type. - * @param {message} string - the message to log - * @param {messageType} string - optional user defined message type + * @param {string} message - The message to log. + * @param {string} [messageType] - User defined message type. */ warning(message: string, messageType?: string): void { this.level(LogLevel.WARNING, message, messageType); @@ -221,16 +251,23 @@ export class Logger { /*@devdoc * Logs a message at the error level, with optional type. - * @param {message} string - the message to log - * @param {messageType} string - optional user defined message type + * @param {string} message - The message to log. + * @param {string} [messageType] - User defined message type. */ error(message: string, messageType?: string): void { this.level(LogLevel.ERROR, message, messageType); } + /*@devdoc + * Predicate function for filtering log messages based on log level. + * @callback LogLevelPredicate + * @param {LogLevel} level - The level in question. + * @return {boolean} Whether to show messages of the specified level. + */ + /*@devdoc * Applies a filer to all subsequently logged messages, based on level. - * @param {pred} (level: LogLevel) => boolean - the filtering condition + * @param {LogLevelPredicate} pred - The filtering condition. */ filterLevels(pred: (level: LogLevel) => boolean): void { this.#_activeFunctions = new Map(); @@ -244,7 +281,7 @@ export class Logger { /*@devdoc * Sets a filter for all subsequently logged messages, based on user defined types. - * @param {filter} Array - the collection of types to allow, + * @param {Array} filter - The collection of types to allow, * presence or presence of undefined value will determine whether to show or hide messages with no type specified. */ setTypeFilter(filter?: Array): void { @@ -268,6 +305,7 @@ export class Logger { /*@devdoc * The Log is the main Logger instance used in the SDK. + * @type {Logger} */ const Log = new Logger(new ConsoleLoggerContext()); From bd1e00f4c63ffd5bf5e11523ce5e1abb243f120a Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 13 Nov 2021 18:35:58 +0400 Subject: [PATCH 10/23] Logger exports moved to the bottom of the file. --- src/domain/shared/Log.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index 39f572c8..50f32794 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -8,8 +8,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - - /*@devdoc * The levels of the Logger class. * @enum {number} @@ -19,7 +17,7 @@ * @property {number} WARNING * @property {number} ERROR */ -export enum LogLevel { +enum LogLevel { DEBUG, DEFAULT, INFO, @@ -31,7 +29,7 @@ export enum LogLevel { * An array containing all values of LogLevel enum in ascending order. * @type {Array} */ -export const allLogLevels = [ +const allLogLevels = [ LogLevel.DEBUG, LogLevel.DEFAULT, LogLevel.INFO, @@ -51,7 +49,7 @@ type LogFunction = (message: string, messageType?: string) => void; * The LoggerContext is an interface for configuration of the output of the Logger class. * @interface LoggerContext */ -export interface LoggerContext { +interface LoggerContext { /*@devdoc * Returns a function that the Logger class would use to log messages at a given level. * @param {LogLevel} level - The level associated with the log function returned. @@ -66,7 +64,7 @@ export interface LoggerContext { * @class ConsoleLoggerContext * @implements LoggerContext */ -export class ConsoleLoggerContext implements LoggerContext { +class ConsoleLoggerContext implements LoggerContext { static #_typeFirst(func: (first: string, second?: string) => void): LogFunction { return (message: string, messageType?: string) => { @@ -123,7 +121,7 @@ export class ConsoleLoggerContext implements LoggerContext { * @implements LoggerContext * @property {string} buffer - The buffer to which all the log messages will be written. */ -export class StringLoggerContext implements LoggerContext { +class StringLoggerContext implements LoggerContext { buffer = ""; @@ -158,7 +156,7 @@ export class StringLoggerContext implements LoggerContext { * @property {number} WARNING - Alias for LogLevel.WARNING. * @property {number} ERROR - Alias for LogLevel.ERROR. */ -export class Logger { +class Logger { readonly DEBUG = LogLevel.DEBUG; readonly DEFAULT = LogLevel.DEFAULT; @@ -310,3 +308,4 @@ export class Logger { const Log = new Logger(new ConsoleLoggerContext()); export default Log; +export { LogLevel, allLogLevels, LoggerContext, ConsoleLoggerContext, StringLoggerContext, Logger }; From 49eb3ecae4761c06408e9eece07ba21c3ea24d3c Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 13 Nov 2021 18:36:44 +0400 Subject: [PATCH 11/23] Minor rename in centralized logger. --- src/domain/shared/Log.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index 50f32794..d0f77f4e 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -29,7 +29,7 @@ enum LogLevel { * An array containing all values of LogLevel enum in ascending order. * @type {Array} */ -const allLogLevels = [ +const AllLogLevels = [ LogLevel.DEBUG, LogLevel.DEFAULT, LogLevel.INFO, @@ -128,7 +128,7 @@ class StringLoggerContext implements LoggerContext { #_functions = new Map(); constructor() { - for (const level of allLogLevels) { + for (const level of AllLogLevels) { this.#_functions.set(level, (message: string, messageType?: string) => { if (messageType) { this.buffer += `[${messageType}]`; @@ -269,7 +269,7 @@ class Logger { */ filterLevels(pred: (level: LogLevel) => boolean): void { this.#_activeFunctions = new Map(); - for (const level of allLogLevels) { + for (const level of AllLogLevels) { const func = this.#_context.getFunction(level); if (func && pred(level)) { this.#_activeFunctions.set(level, func); @@ -308,4 +308,4 @@ class Logger { const Log = new Logger(new ConsoleLoggerContext()); export default Log; -export { LogLevel, allLogLevels, LoggerContext, ConsoleLoggerContext, StringLoggerContext, Logger }; +export { LogLevel, AllLogLevels, LoggerContext, ConsoleLoggerContext, StringLoggerContext, Logger }; From f4c325a9a6865203b3c42644d5acdde239e3a9bb Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 13 Nov 2021 19:00:42 +0400 Subject: [PATCH 12/23] Small simplified in usage of Log in InboundAudioStream. --- src/domain/audio/InboundAudioStream.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/audio/InboundAudioStream.ts b/src/domain/audio/InboundAudioStream.ts index 94d5a549..76fe169c 100644 --- a/src/domain/audio/InboundAudioStream.ts +++ b/src/domain/audio/InboundAudioStream.ts @@ -16,7 +16,7 @@ import { SilentAudioFrameDetails } from "../networking/packets/SilentAudioFrame" import PacketType from "../networking/udt/PacketHeaders"; import UDT from "../networking/udt/UDT"; import ContextManager from "../shared/ContextManager"; -import Log, { LogLevel } from "../shared/Log"; +import Log from "../shared/Log"; /*@devdoc @@ -125,7 +125,7 @@ class InboundAudioStream { // C++ int writeDroppableSilentFrames(int silentFrames) // WEBRTC TODO: Address further C++ code. - Log.once(LogLevel.WARNING, `InboundAudioStream.#writeDroppableSilentFrames() not implemented. Frames: ${silentFrames}`); + Log.once(Log.WARNING, `InboundAudioStream.#writeDroppableSilentFrames() not implemented. Frames: ${silentFrames}`); } From ffa39922c3f3dd93fc38897ed46177d14d09edb1 Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 20 Nov 2021 03:08:14 +0400 Subject: [PATCH 13/23] Major improvements in the centralized logger Better separation between configuration and the logger interface Combination and filtering of logging contexts Single message type replaced with multiple tags Multiple parameters of primitive and record types for message body An interface for logging custom objects Improved ergonomics for one time logging --- src/domain/audio-client/AudioClient.ts | 4 +- src/domain/audio/InboundAudioStream.ts | 2 +- src/domain/shared/Log.ts | 418 ++++++++++++++++--------- tests/domain/shared/Log.unit.test.ts | 176 ++++++----- 4 files changed, 374 insertions(+), 226 deletions(-) diff --git a/src/domain/audio-client/AudioClient.ts b/src/domain/audio-client/AudioClient.ts index 4c9f0e06..e110254c 100644 --- a/src/domain/audio-client/AudioClient.ts +++ b/src/domain/audio-client/AudioClient.ts @@ -435,7 +435,7 @@ class AudioClient { #processStreamStatsPacket = (message: ReceivedMessage, sendingNode: Node | null): void => { // eslint-disable-line // C++ void AudioIOStats::processStreamStatsPacket(ReceivedMessage*, Node* sendingNode) - Log.once(Log.WARNING, "AudioClient: AudioStreamStats packet not processed."); + Log.one.warning("AudioClient: AudioStreamStats packet not processed."); // WEBRTC TODO: Address further C++ code. @@ -447,7 +447,7 @@ class AudioClient { #handleAudioEnvironmentDataPacket = (message: ReceivedMessage): void => { // eslint-disable-line // C++ void handleAudioEnvironmentDataPacket(ReceivedMessage* message) - Log.once(Log.WARNING, "AudioClient: AudioEnvironment packet not processed."); + Log.one.warning("AudioClient: AudioEnvironment packet not processed."); // WEBRTC TODO: Address further C++ code. diff --git a/src/domain/audio/InboundAudioStream.ts b/src/domain/audio/InboundAudioStream.ts index 76fe169c..bbeddad2 100644 --- a/src/domain/audio/InboundAudioStream.ts +++ b/src/domain/audio/InboundAudioStream.ts @@ -125,7 +125,7 @@ class InboundAudioStream { // C++ int writeDroppableSilentFrames(int silentFrames) // WEBRTC TODO: Address further C++ code. - Log.once(Log.WARNING, `InboundAudioStream.#writeDroppableSilentFrames() not implemented. Frames: ${silentFrames}`); + Log.one.warning("InboundAudioStream.#writeDroppableSilentFrames() not implemented. Frames:", silentFrames); } diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index d0f77f4e..000c9a40 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -38,84 +38,162 @@ const AllLogLevels = [ ] as const; /*@devdoc - * The log function type used by in LoggerContext interface. - * @callback LogFunction - * @param {string} message - The main body of the message. + * @typedef {string | symbol | number | bigint | boolean | undefined | null} LoggablePrimitive + */ +type LoggablePrimitive = string | symbol | number | bigint | boolean | undefined | null; + +/*@devdoc + * @typedef {LoggablePrimitive | Record} LoggableRecord + */ +type LoggableRecord = LoggablePrimitive | Record; + +/*@devdoc + * Implementing this interface will allow passing the objects of the class to the {@linkcode Logger} + * @interface Loggable + */ +interface Loggable { + /*@devdoc + * Converts the object to a loggable representation + * @return {LoggableRecord} - a representation of the object based on loggable primitives + */ + toLoggable(): LoggableRecord; +} + +/*@devdoc + * @typedef {LoggablePrimitive | Loggable} LoggableObject + */ +type LoggableObject = LoggablePrimitive | Loggable; + +/*@devdoc + * @typedef {LoggablePrimitive | Loggable} LoggableObjectRecord + */ +type LoggableObjectRecord = LoggableObject | Record + +/*@devdoc + * A predicate for filtering log messages based + * @callback LogMessagePredicate + * @param {LogLevel} level - The logging level of the message. * @param {string} [messageType] - The message type. + * @return {boolean} - Whether to include/allow the message. + */ +type LogMessagePredicate = (level: LogLevel, tags: Set, ...loggables: LoggableRecord[]) => boolean; + +/*@devdoc + * Predicate function for filtering log messages based on log level. + * @callback LogLevelPredicate + * @param {LogLevel} level - The level in question. + * @return {boolean} Whether to show messages of the specified level. */ -type LogFunction = (message: string, messageType?: string) => void; /*@devdoc - * The LoggerContext is an interface for configuration of the output of the Logger class. + * The LoggerContext is an interface used in {@linkcode LoggerConfiguration} to specify the output of the {@linkcode Logger} class. * @interface LoggerContext */ interface LoggerContext { /*@devdoc - * Returns a function that the Logger class would use to log messages at a given level. - * @param {LogLevel} level - The level associated with the log function returned. - * @return {LogFunction | undefined} + * Outputs the log message. + * @param {LogLevel} level - The level to log the message at + * @param {Set} tags - The tags associated with the message + * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. */ - getFunction(level: LogLevel): LogFunction | undefined; + log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void; } /*@devdoc - * The ConsoleLoggerContext is a logger configuration for logging to the dev console. + * The LoggerContext is an interface for configuration of the {@linkcode Logger} class. + * @interface LoggerContext + */ +type LoggerConfiguration = { + + /*@devdoc + * A full message filter that is applied to log input, before it is passed to the context. + * @type {LogMessagePredicate} + */ + filter?: LogMessagePredicate; + + + /*@devdoc + * A level filter that is applied to log input, before the {@linkcode LoggerConfiguration#filter} + * @type {LogLevelPredicate} + */ + levelFilter?: (level: LogLevel) => boolean; + + + /*@devdoc + * A log message tag specific filter that is applied to log input, before the {@linkcode LoggerConfiguration#filter} + * A given message will be included if it has any of the specified tags. undefined value in the array specifies to include messages without any tags. + * @type {Set} + */ + tagFilter?: Array; + + /*@devdoc + * The context that specifies the logger output. + * @type {LoggerContext} + */ + context: LoggerContext; + +} + +function loggablePrimitiveToString(loggable: LoggablePrimitive): string | undefined { + if (loggable == null) { + return "" + loggable; + } else { + switch(typeof loggable) { + case "string": + return loggable; + + case "number": + case "boolean": + case "symbol": + return loggable.toLocaleString(); + + case "bigint": + return BigInt(loggable).toLocaleString(); + + case "undefined": + return "" + loggable; + + default: + return undefined; + } + } +} + +function loggableRecordToString(loggable: Record): string { + let str = "\n" + for (const i in loggable) { + str += ` ${i}: ${loggablePrimitiveToString(loggable[i])}\n`; + } + return str; +} + +function loggableToString(loggable: LoggableRecord) { + return loggablePrimitiveToString(loggable as LoggablePrimitive) || + loggableRecordToString(loggable as Record); +} + +/*@devdoc + * The ConsoleLoggerContext is a logger output configuration for logging to the dev console. * * @class ConsoleLoggerContext * @implements LoggerContext */ class ConsoleLoggerContext implements LoggerContext { - - static #_typeFirst(func: (first: string, second?: string) => void): LogFunction { - return (message: string, messageType?: string) => { - if (messageType) { - return func(`[${messageType}]`, message); + log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { + const logFunction = [console.debug, console.log, console.info, console.warn, console.error].at(level); + if (logFunction) { + if (tags.size === 0) { + logFunction(...loggables); + } else { + logFunction(`[${[...tags].join("][")}]`, ...loggables); } - return func(message); - }; - } - // it is necessary to capture the console object here, for things like jest's mocks to work, - // hence the no-op looking lambda wrappers - static #_functions = new Map([ - /* eslint-disable @typescript-eslint/no-explicit-any */ - [ - LogLevel.DEFAULT, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { - return console.log(...params); - }) - ], - [ - LogLevel.DEBUG, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { - return console.debug(...params); - }) - ], - [ - LogLevel.INFO, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { - return console.info(...params); - }) - ], - [ - LogLevel.WARNING, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { - return console.warn(...params); - }) - ], - [ - LogLevel.ERROR, ConsoleLoggerContext.#_typeFirst((...params: any[]) => { - return console.error(...params); - }) - ] - /* eslint-enable @typescript-eslint/no-explicit-any */ - ]); - - /* eslint-disable class-methods-use-this */ - getFunction(level: LogLevel): LogFunction | undefined { - return ConsoleLoggerContext.#_functions.get(level); + } } - /* eslint-enable class-methods-use-this */ } /*@devdoc - * The StringLoggerContext is a configuration for logging to a string. + * The StringLoggerContext is a output configuration for logging to a string. * * @class StringLoggerContext * @implements LoggerContext @@ -125,31 +203,54 @@ class StringLoggerContext implements LoggerContext { buffer = ""; - #_functions = new Map(); - - constructor() { - for (const level of AllLogLevels) { - this.#_functions.set(level, (message: string, messageType?: string) => { - if (messageType) { - this.buffer += `[${messageType}]`; - } - this.buffer += `[${LogLevel[level] as string}] ${message}\n`; - }); + log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { + if (tags.size !== 0) { + this.buffer += `[${[...tags].join("][")}]`; } + this.buffer += `[${LogLevel[level] as string}]`; + this.buffer += ` ${loggables.map(loggableToString).join(" ")}\n`; } - getFunction(level: LogLevel): LogFunction | undefined { - return this.#_functions.get(level); +} + +class FilteredContext implements LoggerContext +{ + #_context: LoggerContext; + #_filter: LogMessagePredicate; + + constructor(context: LoggerContext, filter: LogMessagePredicate) { + this.#_context = context; + this.#_filter = filter; } + log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { + if (this.#_filter(level, tags, ...loggables)) { + this.#_context.log(level, tags, ...loggables); + } + } } +class LoggerContextCombination implements LoggerContext { + + + #_contexts: [LoggerContext]; + + constructor(...contexts: [LoggerContext]) { + this.#_contexts = contexts; + } + + log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { + for (const context of this.#_contexts) { + context.log(level, tags, ...loggables); + } + } +}; + /*@devdoc * The Logger class serves as a convenience utility and a centralized configuration point for logging. * * @class Logger - * @param {LoggerContext} context - The context this instance will use for output, - * which can also store any addition state, like an output buffer. + * @param {LoggerContext} configuration - The configuration this instance will use for output and filtering. * @property {number} DEBUG - Alias for LogLevel.DEBUG. * @property {number} DEFAULT - Alias for LogLevel.DEFAULT. * @property {number} INFO - Alias for LogLevel.INFO. @@ -164,50 +265,73 @@ class Logger { readonly WARNING = LogLevel.WARNING; readonly ERROR = LogLevel.ERROR; - #_context: LoggerContext; - #_activeFunctions = new Map(); - #_typeFilter?: Array; + #_configuration: LoggerConfiguration; + #_tags = new Set(); + #_tagsKey: string = ""; + #_once = false; + #_messageFlags = new Map(); - #_messageIds = new Map(); - #_typedMessageIds = new Map(); - #_messageFlags = new Map(); + constructor(configuration: LoggerConfiguration) { + this.#_configuration = configuration; + this.#_updateTagsKey(); + } - constructor(context: LoggerContext) { - this.#_context = context; - this.filterLevels(() => { - return true; - }); + tag(...tags: [string]): Logger { + const tagged = new Logger(this.#_configuration); + tagged.#_tags = new Set([...this.#_tags, ...tags]); + tagged.#_updateTagsKey(); + tagged.#_once = this.#_once; + tagged.#_messageFlags = this.#_messageFlags; + return tagged; } - /*@devdoc - * Logs a message at an appropriate level, with optional type. - * @param {LogLevel} level - The level to log at. - * @param {string} message - The message to log. - * @param {string} [messageType] - User defined message type. - */ - level(level: LogLevel, message: string, messageType?: string): void { - const log = this.#_activeFunctions.get(level); - if (log && this.#_isTypeAllowed(messageType)) { - log(message, messageType); - } + once(enabled: boolean): Logger { + const onced = new Logger(this.#_configuration); + onced.#_tags = this.#_tags; + onced.#_tagsKey = this.#_tagsKey; + onced.#_once = enabled; + onced.#_messageFlags = this.#_messageFlags; + return onced; + } + + get one(): Logger { + return this.once(true); } /*@devdoc - * Logs a message at an appropriate level, with optional type, only once. + * Logs a message at an appropriate level, with optional type. * @param {LogLevel} level - The level to log at. * @param {string} message - The message to log. * @param {string} [messageType] - User defined message type. */ - once(level: LogLevel, message: string, messageType?: string): void { - const idMap = messageType ? this.#_typedMessageIds : this.#_messageIds; + level(level: LogLevel, ...loggableObjects: LoggableObjectRecord[]): void { + const loggables = loggableObjects.map((obj) => { + const loggable = obj as Loggable; + if (loggable && loggable.toLoggable) { + return loggable.toLoggable(); + } else { + return obj as LoggableRecord; + } + }) as LoggableRecord[]; - const key = `${messageType || "undefined"} | ${level} | ${message}`; + if (this.#_once) { - const id = Logger.#_getMessageId(idMap, key); + const id = `${this.#_tagsKey} | ${level} | ${loggables.map(loggableToString).join(" | ")}`; + + if (!this.#_messageFlags.get(id)) { + this.#_messageFlags.set(id, true); + } else { + return; + } + } - if (!this.#_messageFlags.get(id)) { - this.#_messageFlags.set(id, true); - this.level(level, message, messageType); + const levelFilter = this.#_configuration.levelFilter; + const tagFilter = this.#_configuration.tagFilter; + const filter = this.#_configuration.filter; + if ((!levelFilter || levelFilter(level)) && + (!tagFilter || tagFilter.filter((tag) => tag === undefined && this.#_tags.size == 0 || this.#_tags.has(tag as string)).length > 0) && + (!filter || filter(level, this.#_tags, ...loggables))) { + this.#_configuration.context.log(level, this.#_tags, ...loggables); } } @@ -216,8 +340,8 @@ class Logger { * @param {string} message - The message to log. * @param {string} [messageType] - User defined message type. */ - message(message: string, messageType?: string): void { - this.level(LogLevel.DEFAULT, message, messageType); + message(...loggables: LoggableObjectRecord[]): void { + this.level(LogLevel.DEFAULT, ...loggables); } /*@devdoc @@ -225,8 +349,8 @@ class Logger { * @param {string} message - The message to log. * @param {string} [messageType] - User defined message type. */ - info(message: string, messageType?: string): void { - this.level(LogLevel.INFO, message, messageType); + info(...loggables: LoggableObjectRecord[]): void { + this.level(LogLevel.INFO, ...loggables); } /*@devdoc @@ -234,8 +358,8 @@ class Logger { * @param {string} message - The message to log. * @param {string} [messageType] - User defined message type. */ - debug(message: string, messageType?: string): void { - this.level(LogLevel.DEBUG, message, messageType); + debug(...loggables: LoggableObjectRecord[]): void { + this.level(LogLevel.DEBUG, ...loggables); } /*@devdoc @@ -243,8 +367,8 @@ class Logger { * @param {string} message - The message to log. * @param {string} [messageType] - User defined message type. */ - warning(message: string, messageType?: string): void { - this.level(LogLevel.WARNING, message, messageType); + warning(...loggables: LoggableObjectRecord[]): void { + this.level(LogLevel.WARNING, ...loggables); } /*@devdoc @@ -252,60 +376,50 @@ class Logger { * @param {string} message - The message to log. * @param {string} [messageType] - User defined message type. */ - error(message: string, messageType?: string): void { - this.level(LogLevel.ERROR, message, messageType); + error(...loggables: LoggableObjectRecord[]): void { + this.level(LogLevel.ERROR, ...loggables); } - /*@devdoc - * Predicate function for filtering log messages based on log level. - * @callback LogLevelPredicate - * @param {LogLevel} level - The level in question. - * @return {boolean} Whether to show messages of the specified level. - */ - - /*@devdoc - * Applies a filer to all subsequently logged messages, based on level. - * @param {LogLevelPredicate} pred - The filtering condition. - */ - filterLevels(pred: (level: LogLevel) => boolean): void { - this.#_activeFunctions = new Map(); - for (const level of AllLogLevels) { - const func = this.#_context.getFunction(level); - if (func && pred(level)) { - this.#_activeFunctions.set(level, func); - } - } - } - - /*@devdoc - * Sets a filter for all subsequently logged messages, based on user defined types. - * @param {Array} filter - The collection of types to allow, - * presence or presence of undefined value will determine whether to show or hide messages with no type specified. - */ - setTypeFilter(filter?: Array): void { - this.#_typeFilter = filter; - } - - #_isTypeAllowed(messageType?: string): boolean { - return !this.#_typeFilter || this.#_typeFilter.includes(messageType); - } - - static #_getMessageId(idMap: Map, key: string): unknown { - if (!idMap.has(key)) { - const id = {}; - idMap.set(key, id); - return id; - } - return idMap.get(key); + #_updateTagsKey() + { + const tagSizes = [...this.#_tags].map(tag => tag.length).join(","); + const tagsString = [...this.#_tags].join(); + this.#_tagsKey = `${tagSizes} | ${tagsString}`; } } +/*@sdkdoc + * The DefaultLogConfiguration provides a configuration point for SDK logger. + * + * @namespace DefaultLogConfiguration + * @property {LoggerContext} context - The context that specifies the output destination for the logs. + * @property {LogMessagePredicate} [filter] - A full message filter that is applied to log input, before it is passed to the context. + */ +const DefaultLogConfiguration = { + context: new ConsoleLoggerContext(), + levelFilter: (level: LogLevel) => level >= LogLevel.WARNING +} as LoggerConfiguration; + /*@devdoc * The Log is the main Logger instance used in the SDK. * @type {Logger} */ -const Log = new Logger(new ConsoleLoggerContext()); +const Log = new Logger(DefaultLogConfiguration); + +process.env export default Log; -export { LogLevel, AllLogLevels, LoggerContext, ConsoleLoggerContext, StringLoggerContext, Logger }; +export { + LogLevel, + AllLogLevels, + Loggable, + LoggerContext, + LoggerConfiguration, + ConsoleLoggerContext, + StringLoggerContext, + FilteredContext, + LoggerContextCombination, + Logger, + DefaultLogConfiguration +}; diff --git a/tests/domain/shared/Log.unit.test.ts b/tests/domain/shared/Log.unit.test.ts index 3d730cd1..abcc73e1 100644 --- a/tests/domain/shared/Log.unit.test.ts +++ b/tests/domain/shared/Log.unit.test.ts @@ -8,13 +8,13 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import { Logger, LogLevel, ConsoleLoggerContext, StringLoggerContext } from "../../../src/domain/shared/Log"; +import { Logger, LogLevel, LoggerConfiguration, ConsoleLoggerContext, StringLoggerContext } from "../../../src/domain/shared/Log"; describe("Logger - unit tests", () => { test("Console context", () => { - const logger = new Logger(new ConsoleLoggerContext()); + const logger = new Logger({context: new ConsoleLoggerContext()}); const debug = jest.spyOn(console, "debug").mockImplementation(() => { /* no-op */ }); const log = jest.spyOn(console, "log").mockImplementation(() => { /* no-op */ }); @@ -40,11 +40,11 @@ describe("Logger - unit tests", () => { warn.mockClear(); error.mockClear(); - logger.debug("Console debug message", "Type 1"); - logger.message("Console log message", "Type 2"); - logger.info("Console info message", "Type 3"); - logger.warning("Console warning message", "Type 4"); - logger.error("Console error message", "Type 5"); + logger.tag("Type 1").debug("Console debug message"); + logger.tag("Type 2").message("Console log message"); + logger.tag("Type 3").info("Console info message"); + logger.tag("Type 4").warning("Console warning message"); + logger.tag("Type 5").error("Console error message"); expect(debug).toHaveBeenCalledWith("[Type 1]", "Console debug message"); expect(log).toHaveBeenCalledWith("[Type 2]", "Console log message"); @@ -62,7 +62,7 @@ describe("Logger - unit tests", () => { test("String context", () => { const context = new StringLoggerContext(); - const logger = new Logger(context); + const logger = new Logger({context}); logger.debug("Debug message"); logger.message("Default message"); @@ -79,11 +79,11 @@ describe("Logger - unit tests", () => { ); context.buffer = ""; - logger.debug("Debug message", "Type 1"); - logger.message("Default message", "Type 2"); - logger.info("Info message", "Type 3"); - logger.warning("Warning message", "Type 4"); - logger.error("Error message", "Type 5"); + logger.tag("Type 1").debug("Debug message"); + logger.tag("Type 2").message("Default message"); + logger.tag("Type 3").info("Info message"); + logger.tag("Type 4").warning("Warning message"); + logger.tag("Type 5").error("Error message"); expect(context.buffer).toBe("" + "[Type 1][DEBUG] Debug message\n" @@ -96,11 +96,12 @@ describe("Logger - unit tests", () => { test("Filtering by level", () => { const context = new StringLoggerContext(); - const logger = new Logger(context); + const config = {context} as LoggerConfiguration; + const logger = new Logger(config); - logger.filterLevels((level) => { + config.levelFilter = (level) => { return level >= LogLevel.INFO; - }); + }; logger.debug("Debug message"); logger.message("Default message"); @@ -115,9 +116,9 @@ describe("Logger - unit tests", () => { ); context.buffer = ""; - logger.filterLevels((level) => { + config.levelFilter = (level) => { return level <= LogLevel.DEFAULT; - }); + }; logger.debug("Debug message"); logger.message("Default message"); @@ -131,9 +132,9 @@ describe("Logger - unit tests", () => { ); context.buffer = ""; - logger.filterLevels((level) => { + config.levelFilter = (level) => { return [LogLevel.DEBUG, LogLevel.ERROR].includes(level); - }); + }; logger.debug("Debug message"); logger.message("Default message"); @@ -150,16 +151,17 @@ describe("Logger - unit tests", () => { test("Filtering by type", () => { const context = new StringLoggerContext(); - const logger = new Logger(context); + const config = {context} as LoggerConfiguration; + const logger = new Logger(config); - logger.setTypeFilter(["Type 2", "Type 4"]); + config.tagFilter = ["Type 2", "Type 4"]; logger.message("message"); - logger.message("message 1", "Type 1"); - logger.message("message 2", "Type 2"); - logger.message("message 3", "Type 3"); - logger.message("message 4", "Type 4"); - logger.message("message 5", "Type 5"); + logger.tag("Type 1").message("message 1"); + logger.tag("Type 2").message("message 2"); + logger.tag("Type 3").message("message 3"); + logger.tag("Type 4").message("message 4"); + logger.tag("Type 5").message("message 5"); expect(context.buffer).toBe("" + "[Type 2][DEFAULT] message 2\n" @@ -167,14 +169,14 @@ describe("Logger - unit tests", () => { ); context.buffer = ""; - logger.setTypeFilter(["Type 1", "Type 3", undefined]); + config.tagFilter = ["Type 1", "Type 3", undefined]; logger.message("message"); - logger.message("message 1", "Type 1"); - logger.message("message 2", "Type 2"); - logger.message("message 3", "Type 3"); - logger.message("message 4", "Type 4"); - logger.message("message 5", "Type 5"); + logger.tag("Type 1").message("message 1"); + logger.tag("Type 2").message("message 2"); + logger.tag("Type 3").message("message 3"); + logger.tag("Type 4").message("message 4"); + logger.tag("Type 5").message("message 5"); expect(context.buffer).toBe("" + "[DEFAULT] message\n" @@ -183,28 +185,28 @@ describe("Logger - unit tests", () => { ); context.buffer = ""; - logger.setTypeFilter([undefined]); + config.tagFilter = [undefined]; logger.message("message"); - logger.message("message 1", "Type 1"); - logger.message("message 2", "Type 2"); - logger.message("message 3", "Type 3"); - logger.message("message 4", "Type 4"); - logger.message("message 5", "Type 5"); + logger.tag("Type 1").message("message 1"); + logger.tag("Type 2").message("message 2"); + logger.tag("Type 3").message("message 3"); + logger.tag("Type 4").message("message 4"); + logger.tag("Type 5").message("message 5"); expect(context.buffer).toBe("" + "[DEFAULT] message\n" ); context.buffer = ""; - logger.setTypeFilter(); + config.tagFilter = undefined; logger.message("message"); - logger.message("message 1", "Type 1"); - logger.message("message 2", "Type 2"); - logger.message("message 3", "Type 3"); - logger.message("message 4", "Type 4"); - logger.message("message 5", "Type 5"); + logger.tag("Type 1").message("message 1"); + logger.tag("Type 2").message("message 2"); + logger.tag("Type 3").message("message 3"); + logger.tag("Type 4").message("message 4"); + logger.tag("Type 5").message("message 5"); expect(context.buffer).toBe("" + "[DEFAULT] message\n" @@ -220,19 +222,20 @@ describe("Logger - unit tests", () => { test("Mixed filtering", () => { const context = new StringLoggerContext(); - const logger = new Logger(context); + const config = {context} as LoggerConfiguration; + const logger = new Logger(config); - logger.filterLevels((level) => { + config.levelFilter = (level) => { return level <= LogLevel.DEFAULT; - }); - logger.setTypeFilter(["Type 2"]); + }; + config.tagFilter = ["Type 2"]; logger.message("message"); - logger.debug("Debug message 1", "Type 1"); - logger.message("Default message 2", "Type 2"); - logger.info("Info message 1", "Type 1"); - logger.warning("message 2", "Type 2"); - logger.error("message 1", "Type 1"); + logger.tag("Type 1").debug("Debug message 1"); + logger.tag("Type 2").message("Default message 2"); + logger.tag("Type 3").info("Info message 1"); + logger.tag("Type 4").warning("message 2"); + logger.tag("Type 5").error("message 1"); expect(context.buffer).toBe("" + "[Type 2][DEFAULT] Default message 2\n" @@ -240,29 +243,30 @@ describe("Logger - unit tests", () => { context.buffer = ""; }); - test("Logger.once", () => { + test("Log a message only once", () => { const context = new StringLoggerContext(); - const logger = new Logger(context); + const config = {context} as LoggerConfiguration; + const logger = new Logger(config); - logger.once(LogLevel.DEBUG, "message"); - logger.once(LogLevel.DEBUG, "message"); - logger.once(LogLevel.DEBUG, "message"); + logger.one.debug("message"); + logger.one.debug("message"); + logger.one.debug("message"); - logger.once(LogLevel.DEBUG, "message", "Type"); - logger.once(LogLevel.DEBUG, "message", "Type"); - logger.once(LogLevel.DEBUG, "message", "Type"); + logger.tag("Type").one.debug("message"); + logger.tag("Type").one.debug("message"); + logger.tag("Type").one.debug("message"); - logger.once(LogLevel.DEBUG, "message", "undefined"); - logger.once(LogLevel.DEBUG, "message", "undefined"); - logger.once(LogLevel.DEBUG, "message", "undefined"); + logger.tag("undefined").one.debug("message"); + logger.tag("undefined").one.debug("message"); + logger.tag("undefined").one.debug("message"); - logger.once(LogLevel.INFO, "message"); - logger.once(LogLevel.INFO, "message"); - logger.once(LogLevel.INFO, "message"); + logger.one.info("message"); + logger.one.info("message"); + logger.one.info("message"); - logger.once(LogLevel.DEBUG, "another message"); - logger.once(LogLevel.DEBUG, "another message"); - logger.once(LogLevel.DEBUG, "another message"); + logger.one.debug("another message"); + logger.one.debug("another message"); + logger.one.debug("another message"); expect(context.buffer).toBe("" + "[DEBUG] message\n" @@ -274,4 +278,34 @@ describe("Logger - unit tests", () => { context.buffer = ""; }); + test("Log a record", () => { + const context = new StringLoggerContext(); + const logger = new Logger({context}); + + const big = BigInt("9999999999999999999999999999999999999999999999"); + + logger.message("a record is:", { + str: "str", + sym: Symbol("sym"), + num: 2, + bnum: big, + bool: true, + und: undefined, + nil: null + }); + + expect(context.buffer).toBe("" + + "[DEFAULT] a record is: \n" + + " str: str\n" + + " sym: Symbol(sym)\n" + + " num: 2\n" + + ` bnum: ${big.toLocaleString()}\n` + + " bool: true\n" + + " und: undefined\n" + + " nil: null\n" + + "\n" + ); + context.buffer = ""; + }); + }); From ad2b1c6cb06ee4446455dbb488501f9ab1e7af6a Mon Sep 17 00:00:00 2001 From: namark Date: Mon, 22 Nov 2021 17:52:48 +0400 Subject: [PATCH 14/23] Small improvements and fixes in centralized logger. Updated unit test and documentation. --- src/domain/shared/Log.ts | 277 +++++++++++++++++++-------- tests/domain/shared/Log.unit.test.ts | 150 ++++++++++++++- 2 files changed, 336 insertions(+), 91 deletions(-) diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index 000c9a40..c6e5e31a 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -52,7 +52,7 @@ type LoggableRecord = LoggablePrimitive | Record; * @interface Loggable */ interface Loggable { - /*@devdoc + /*devdoc * Converts the object to a loggable representation * @return {LoggableRecord} - a representation of the object based on loggable primitives */ @@ -67,13 +67,14 @@ type LoggableObject = LoggablePrimitive | Loggable; /*@devdoc * @typedef {LoggablePrimitive | Loggable} LoggableObjectRecord */ -type LoggableObjectRecord = LoggableObject | Record +type LoggableObjectRecord = LoggableObject | Record; /*@devdoc - * A predicate for filtering log messages based + * A predicate for filtering log messages * @callback LogMessagePredicate * @param {LogLevel} level - The logging level of the message. - * @param {string} [messageType] - The message type. + * @param {Set} tags - The tags associated with the message. + * @return {...LoggableRecord} loggables - the objects that comprise the message body * @return {boolean} - Whether to include/allow the message. */ type LogMessagePredicate = (level: LogLevel, tags: Set, ...loggables: LoggableRecord[]) => boolean; @@ -86,28 +87,32 @@ type LogMessagePredicate = (level: LogLevel, tags: Set, ...loggables: Lo */ /*@devdoc - * The LoggerContext is an interface used in {@linkcode LoggerConfiguration} to specify the output of the {@linkcode Logger} class. + * The LoggerContext is an interface used in {@linkcode LoggerConfiguration} + * to specify the output of the {@linkcode Logger} class. * @interface LoggerContext */ interface LoggerContext { /*@devdoc * Outputs the log message. - * @param {LogLevel} level - The level to log the message at - * @param {Set} tags - The tags associated with the message + * @function + * @name LoggerContext#log + * @param {LogLevel} level - The level to log the message at. + * @param {Set} tags - The tags associated with the message. * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void; } /*@devdoc - * The LoggerContext is an interface for configuration of the {@linkcode Logger} class. - * @interface LoggerContext + * The LoggerConfiguration is an interface for configuration of the {@linkcode Logger} class. + * @interface LoggerConfiguration */ type LoggerConfiguration = { /*@devdoc * A full message filter that is applied to log input, before it is passed to the context. * @type {LogMessagePredicate} + * @name [LoggerConfiguration#filter] */ filter?: LogMessagePredicate; @@ -115,61 +120,70 @@ type LoggerConfiguration = { /*@devdoc * A level filter that is applied to log input, before the {@linkcode LoggerConfiguration#filter} * @type {LogLevelPredicate} + * @name [LoggerConfiguration#levelFilter] */ levelFilter?: (level: LogLevel) => boolean; /*@devdoc * A log message tag specific filter that is applied to log input, before the {@linkcode LoggerConfiguration#filter} - * A given message will be included if it has any of the specified tags. undefined value in the array specifies to include messages without any tags. - * @type {Set} + * A given message will be included if it has any of the specified tags. + * undefined value in the array specifies to include messages without any tags. + * @type {Array} + * @name [LoggerConfiguration#tagFilter] */ tagFilter?: Array; /*@devdoc * The context that specifies the logger output. * @type {LoggerContext} + * @name LoggerConfiguration#context */ context: LoggerContext; -} +}; function loggablePrimitiveToString(loggable: LoggablePrimitive): string | undefined { - if (loggable == null) { - return "" + loggable; - } else { - switch(typeof loggable) { - case "string": - return loggable; - - case "number": - case "boolean": - case "symbol": - return loggable.toLocaleString(); - - case "bigint": - return BigInt(loggable).toLocaleString(); - - case "undefined": - return "" + loggable; - - default: - return undefined; - } + if (loggable === null) { + // NOTE: eslint disallows both + // "" + loggable + // and + // `${loggable}` + // I'm out of ideas, also for the undefined case below + return "null"; + } + + switch (typeof loggable) { + case "string": + return loggable; + + case "number": + case "boolean": + case "symbol": + return loggable.toLocaleString(); + + case "bigint": + return BigInt(loggable).toLocaleString(); + + case "undefined": + return "undefined"; + + default: + return undefined; } } function loggableRecordToString(loggable: Record): string { - let str = "\n" - for (const i in loggable) { - str += ` ${i}: ${loggablePrimitiveToString(loggable[i])}\n`; + let str = "\n"; + for (const [key, value] of Object.entries(loggable)) { + str += ` ${key}: ${loggablePrimitiveToString(value) as string}\n`; } return str; } function loggableToString(loggable: LoggableRecord) { - return loggablePrimitiveToString(loggable as LoggablePrimitive) || - loggableRecordToString(loggable as Record); + return loggablePrimitiveToString(loggable as LoggablePrimitive) + || loggableRecordToString(loggable as Record); } /*@devdoc @@ -179,6 +193,13 @@ function loggableToString(loggable: LoggableRecord) { * @implements LoggerContext */ class ConsoleLoggerContext implements LoggerContext { + /* eslint-disable class-methods-use-this */ + /*@devdoc + * Outputs the log to the developer console. + * @param {LogLevel} level - The level to log the message at. + * @param {Set} tags - The tags associated with the message. + * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { const logFunction = [console.debug, console.log, console.info, console.warn, console.error].at(level); if (logFunction) { @@ -187,9 +208,9 @@ class ConsoleLoggerContext implements LoggerContext { } else { logFunction(`[${[...tags].join("][")}]`, ...loggables); } - } } + /* eslint-enable class-methods-use-this */ } /*@devdoc @@ -203,6 +224,12 @@ class StringLoggerContext implements LoggerContext { buffer = ""; + /*@devdoc + * Outputs the log to the string buffer. + * @param {LogLevel} level - The level to log the message at. + * @param {Set} tags - The tags associated with the message. + * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { if (tags.size !== 0) { this.buffer += `[${[...tags].join("][")}]`; @@ -213,8 +240,16 @@ class StringLoggerContext implements LoggerContext { } -class FilteredContext implements LoggerContext -{ +/*@devdoc + * The LogFilterContext is a logger context wrapper for filtering messages. + * + * @class LogFilterContext + * @implements LoggerContext + * @param {LoggerContext} context - The buffer to forward the filtered logs to. + * @param {LogMessagePredicate} filter - The filter to apply to the messages. + */ +class LogFilterContext implements LoggerContext { + #_context: LoggerContext; #_filter: LogMessagePredicate; @@ -223,6 +258,12 @@ class FilteredContext implements LoggerContext this.#_filter = filter; } + /*@devdoc + * Passes logs that satisfy the specified filter to the specified context. + * @param {LogLevel} level - The level to log the message at. + * @param {Set} tags - The tags associated with the message. + * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { if (this.#_filter(level, tags, ...loggables)) { this.#_context.log(level, tags, ...loggables); @@ -230,32 +271,89 @@ class FilteredContext implements LoggerContext } } +/*@devdoc + * The LoggerContextCombination combines several logger contexts into one. + * + * @class LoggerContextCombination + * @implements LoggerContext + * @param {...LoggerContext} contexts - The contexts to combine. + */ class LoggerContextCombination implements LoggerContext { + #_contexts: LoggerContext[]; - #_contexts: [LoggerContext]; - - constructor(...contexts: [LoggerContext]) { + constructor(...contexts: LoggerContext[]) { this.#_contexts = contexts; } + /*@devdoc + * Passes log message to all specified contexts. + * @param {LogLevel} level - The level to log the message at. + * @param {Set} tags - The tags associated with the message. + * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { for (const context of this.#_contexts) { context.log(level, tags, ...loggables); } } -}; +} + +/*@devdoc + * The LogReportContext buffers log messages and passes them to specified context + * when a message is logged at the ERROR level or above. + * + * @class LogReportContext + * @implements LoggerContext + * @param {LoggerContext} context - The contexts to pass the messages to. + * @param {number} [logCount] - The number of log messages to buffer. + * If unspecified all messages will be buffered. + */ +class LogReportContext implements LoggerContext { + + #_context: LoggerContext; + #_logCount?: number; + #_report: Array<[LogLevel, Set, LoggableRecord[]]> = []; + + constructor(context: LoggerContext, logCount?: number) { + this.#_context = context; + this.#_logCount = logCount; + } + + /*@devdoc + * Buffers the logs and passes them to the specified context. + * @param {LogLevel} level - The level to log the message at. + * @param {Set} tags - The tags associated with the message. + * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + */ + log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { + + this.#_report.push([level, tags, loggables]); + + if (this.#_logCount && this.#_report.length > this.#_logCount) { + this.#_report.shift(); + } + + if (level >= LogLevel.ERROR) { + for (const log of this.#_report) { + this.#_context.log(log[0], log[1], ...log[2]); + } + this.#_report.length = 0; + } + } +} /*@devdoc * The Logger class serves as a convenience utility and a centralized configuration point for logging. * * @class Logger - * @param {LoggerContext} configuration - The configuration this instance will use for output and filtering. + * @param {LoggerConfiguration} configuration - The configuration this instance will use for output and filtering. * @property {number} DEBUG - Alias for LogLevel.DEBUG. * @property {number} DEFAULT - Alias for LogLevel.DEFAULT. * @property {number} INFO - Alias for LogLevel.INFO. * @property {number} WARNING - Alias for LogLevel.WARNING. * @property {number} ERROR - Alias for LogLevel.ERROR. + * @property {Logger} one - Equivalent to calling {@linkcode Logger#once} with true */ class Logger { @@ -267,7 +365,7 @@ class Logger { #_configuration: LoggerConfiguration; #_tags = new Set(); - #_tagsKey: string = ""; + #_tagsKey = ""; #_once = false; #_messageFlags = new Map(); @@ -276,7 +374,12 @@ class Logger { this.#_updateTagsKey(); } - tag(...tags: [string]): Logger { + /*@devdoc + * Copy this logger instance adding specified tags. + * @param {...string} tags - The tags to add to the new logger instance. + * @return {Logger} - A new logger instance with specified tags added. + */ + tag(...tags: string[]): Logger { const tagged = new Logger(this.#_configuration); tagged.#_tags = new Set([...this.#_tags, ...tags]); tagged.#_updateTagsKey(); @@ -285,6 +388,11 @@ class Logger { return tagged; } + /*@devdoc + * Copy this logger instance specifying whether to eliminate duplicate logs or not. + * @param {boolean} enabled - true - prevent duplicates, false - allow duplicates. + * @return {Logger} - A new logger instance with specified behavior. + */ once(enabled: boolean): Logger { const onced = new Logger(this.#_configuration); onced.#_tags = this.#_tags; @@ -299,20 +407,18 @@ class Logger { } /*@devdoc - * Logs a message at an appropriate level, with optional type. + * Logs a message at the specified level. * @param {LogLevel} level - The level to log at. - * @param {string} message - The message to log. - * @param {string} [messageType] - User defined message type. + * @param {...LoggableObjectRecord} loggableObjects - The objects that comprise the message body. */ level(level: LogLevel, ...loggableObjects: LoggableObjectRecord[]): void { const loggables = loggableObjects.map((obj) => { const loggable = obj as Loggable; if (loggable && loggable.toLoggable) { return loggable.toLoggable(); - } else { - return obj as LoggableRecord; } - }) as LoggableRecord[]; + return obj as LoggableRecord; + }); if (this.#_once) { @@ -328,61 +434,60 @@ class Logger { const levelFilter = this.#_configuration.levelFilter; const tagFilter = this.#_configuration.tagFilter; const filter = this.#_configuration.filter; - if ((!levelFilter || levelFilter(level)) && - (!tagFilter || tagFilter.filter((tag) => tag === undefined && this.#_tags.size == 0 || this.#_tags.has(tag as string)).length > 0) && - (!filter || filter(level, this.#_tags, ...loggables))) { + if ((!levelFilter || levelFilter(level)) + && (!tagFilter || tagFilter.filter((tag) => { + return tag === undefined && this.#_tags.size === 0 + || this.#_tags.has(tag as string); + }).length > 0) + && (!filter || filter(level, this.#_tags, ...loggables))) { this.#_configuration.context.log(level, this.#_tags, ...loggables); } } /*@devdoc - * Logs a message at the default level, with optional type. - * @param {string} message - The message to log. - * @param {string} [messageType] - User defined message type. + * Logs a message at the default level. + * @param {...LoggableObjectRecord} loggables - The objects that comprise the message body. */ message(...loggables: LoggableObjectRecord[]): void { this.level(LogLevel.DEFAULT, ...loggables); } /*@devdoc - * Logs a message at the info level, with optional type. - * @param {string} message - The message to log. - * @param {string} [messageType] - User defined message type. + * Logs a message at the info level. + * @param {...LoggableObjectRecord} loggables - The objects that comprise the message body. */ info(...loggables: LoggableObjectRecord[]): void { this.level(LogLevel.INFO, ...loggables); } /*@devdoc - * Logs a message at the debug level, with optional type. - * @param {string} message - The message to log. - * @param {string} [messageType] - User defined message type. + * Logs a message at the debug level. + * @param {...LoggableObjectRecord} loggables - The objects that comprise the message body. */ debug(...loggables: LoggableObjectRecord[]): void { this.level(LogLevel.DEBUG, ...loggables); } /*@devdoc - * Logs a message at the warning level, with optional type. - * @param {string} message - The message to log. - * @param {string} [messageType] - User defined message type. + * Logs a message at the warning level. + * @param {...LoggableObjectRecord} loggables - The objects that comprise the message body. */ warning(...loggables: LoggableObjectRecord[]): void { this.level(LogLevel.WARNING, ...loggables); } /*@devdoc - * Logs a message at the error level, with optional type. - * @param {string} message - The message to log. - * @param {string} [messageType] - User defined message type. + * Logs a message at the error level. + * @param {...LoggableObjectRecord} loggables - The objects that comprise the message body. */ error(...loggables: LoggableObjectRecord[]): void { this.level(LogLevel.ERROR, ...loggables); } - #_updateTagsKey() - { - const tagSizes = [...this.#_tags].map(tag => tag.length).join(","); + #_updateTagsKey(): void { + const tagSizes = [...this.#_tags].map((tag) => { + return tag.length; + }).join(","); const tagsString = [...this.#_tags].join(); this.#_tagsKey = `${tagSizes} | ${tagsString}`; } @@ -390,25 +495,32 @@ class Logger { } /*@sdkdoc - * The DefaultLogConfiguration provides a configuration point for SDK logger. + * The DefaultLogConfiguration is the main {@linkcode LoggerConfiguration} object used by the SDK. * * @namespace DefaultLogConfiguration * @property {LoggerContext} context - The context that specifies the output destination for the logs. - * @property {LogMessagePredicate} [filter] - A full message filter that is applied to log input, before it is passed to the context. + * @property {LogMessagePredicate} [filter] - A full message filter that is applied to log input, + * before it is passed to the context. + * @property {LogLevelPredicate} [levelFilter] - A level filter that is applied to log input, + * before the {@linkcode DefaultLogConfiguration#filter} + * @property {Array} [tagFilter] - A log message tag specific filter that is applied to log input, + * before the {@linkcode DefaultLogConfiguration#filter}. + * A given message will be included if it has any of the specified tags. + * undefined value in the array specifies to include messages without any tags. */ const DefaultLogConfiguration = { context: new ConsoleLoggerContext(), - levelFilter: (level: LogLevel) => level >= LogLevel.WARNING + levelFilter: (level: LogLevel) => { + return level >= LogLevel.WARNING; + } } as LoggerConfiguration; /*@devdoc - * The Log is the main Logger instance used in the SDK. + * The Log is the main {@linkcode Logger} instance used in the SDK. * @type {Logger} */ const Log = new Logger(DefaultLogConfiguration); -process.env - export default Log; export { LogLevel, @@ -418,8 +530,9 @@ export { LoggerConfiguration, ConsoleLoggerContext, StringLoggerContext, - FilteredContext, + LogFilterContext, LoggerContextCombination, + LogReportContext, Logger, DefaultLogConfiguration }; diff --git a/tests/domain/shared/Log.unit.test.ts b/tests/domain/shared/Log.unit.test.ts index abcc73e1..472def96 100644 --- a/tests/domain/shared/Log.unit.test.ts +++ b/tests/domain/shared/Log.unit.test.ts @@ -8,13 +8,22 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import { Logger, LogLevel, LoggerConfiguration, ConsoleLoggerContext, StringLoggerContext } from "../../../src/domain/shared/Log"; +import { + Logger, + LogLevel, + LoggerConfiguration, + ConsoleLoggerContext, + StringLoggerContext, + LoggerContextCombination, + LogFilterContext, + LogReportContext +} from "../../../src/domain/shared/Log"; describe("Logger - unit tests", () => { test("Console context", () => { - const logger = new Logger({context: new ConsoleLoggerContext()}); + const logger = new Logger({ context: new ConsoleLoggerContext() }); const debug = jest.spyOn(console, "debug").mockImplementation(() => { /* no-op */ }); const log = jest.spyOn(console, "log").mockImplementation(() => { /* no-op */ }); @@ -62,7 +71,7 @@ describe("Logger - unit tests", () => { test("String context", () => { const context = new StringLoggerContext(); - const logger = new Logger({context}); + const logger = new Logger({ context }); logger.debug("Debug message"); logger.message("Default message"); @@ -96,7 +105,7 @@ describe("Logger - unit tests", () => { test("Filtering by level", () => { const context = new StringLoggerContext(); - const config = {context} as LoggerConfiguration; + const config = { context } as LoggerConfiguration; const logger = new Logger(config); config.levelFilter = (level) => { @@ -149,9 +158,9 @@ describe("Logger - unit tests", () => { }); - test("Filtering by type", () => { + test("Filtering by tags", () => { const context = new StringLoggerContext(); - const config = {context} as LoggerConfiguration; + const config = { context } as LoggerConfiguration; const logger = new Logger(config); config.tagFilter = ["Type 2", "Type 4"]; @@ -222,7 +231,7 @@ describe("Logger - unit tests", () => { test("Mixed filtering", () => { const context = new StringLoggerContext(); - const config = {context} as LoggerConfiguration; + const config = { context } as LoggerConfiguration; const logger = new Logger(config); config.levelFilter = (level) => { @@ -245,7 +254,7 @@ describe("Logger - unit tests", () => { test("Log a message only once", () => { const context = new StringLoggerContext(); - const config = {context} as LoggerConfiguration; + const config = { context } as LoggerConfiguration; const logger = new Logger(config); logger.one.debug("message"); @@ -280,7 +289,7 @@ describe("Logger - unit tests", () => { test("Log a record", () => { const context = new StringLoggerContext(); - const logger = new Logger({context}); + const logger = new Logger({ context }); const big = BigInt("9999999999999999999999999999999999999999999999"); @@ -308,4 +317,127 @@ describe("Logger - unit tests", () => { context.buffer = ""; }); + test("Multiple tags", () => { + const context = new StringLoggerContext(); + const config = { context } as LoggerConfiguration; + const logger = new Logger(config).tag("toplevel"); + + logger.tag("one", "two").message("message"); + logger.tag("two", "three").message("message"); + logger.tag("four", "five").message("message"); + + expect(context.buffer).toBe("" + + "[toplevel][one][two][DEFAULT] message\n" + + "[toplevel][two][three][DEFAULT] message\n" + + "[toplevel][four][five][DEFAULT] message\n" + ); + context.buffer = ""; + + config.tagFilter = ["one"]; + + logger.tag("one", "two").message("message"); + logger.tag("two", "three").message("message"); + logger.tag("four", "one").message("message"); + + expect(context.buffer).toBe("" + + "[toplevel][one][two][DEFAULT] message\n" + + "[toplevel][four][one][DEFAULT] message\n" + ); + + }); + + test("Contexts composition", () => { + const context1 = new StringLoggerContext(); + const context2 = new StringLoggerContext(); + const filtered = new StringLoggerContext(); + const buglog = new StringLoggerContext(); + + + const logger = new Logger({ + context: new LoggerContextCombination( + context1, + context2, + new LogFilterContext(filtered, + (level: LogLevel) => level === LogLevel.INFO), + new LogReportContext(buglog, 3) + ) + }); + + logger.message("message 1"); + logger.message("message 2"); + logger.message("message 3"); + + expect(context1.buffer).toBe("" + + "[DEFAULT] message 1\n" + + "[DEFAULT] message 2\n" + + "[DEFAULT] message 3\n" + ); + + expect(context2.buffer).toBe("" + + "[DEFAULT] message 1\n" + + "[DEFAULT] message 2\n" + + "[DEFAULT] message 3\n" + ); + + expect(filtered.buffer).toBe(""); + expect(buglog.buffer).toBe(""); + + logger.info("message 4"); + logger.warning("minor"); + + expect(context1.buffer).toBe("" + + "[DEFAULT] message 1\n" + + "[DEFAULT] message 2\n" + + "[DEFAULT] message 3\n" + + "[INFO] message 4\n" + + "[WARNING] minor\n" + ); + + expect(context2.buffer).toBe("" + + "[DEFAULT] message 1\n" + + "[DEFAULT] message 2\n" + + "[DEFAULT] message 3\n" + + "[INFO] message 4\n" + + "[WARNING] minor\n" + ); + + expect(filtered.buffer).toBe("" + + "[INFO] message 4\n" + ); + expect(buglog.buffer).toBe(""); + + logger.error("fatal"); + + expect(context1.buffer).toBe("" + + "[DEFAULT] message 1\n" + + "[DEFAULT] message 2\n" + + "[DEFAULT] message 3\n" + + "[INFO] message 4\n" + + "[WARNING] minor\n" + + "[ERROR] fatal\n" + ); + + expect(context2.buffer).toBe("" + + "[DEFAULT] message 1\n" + + "[DEFAULT] message 2\n" + + "[DEFAULT] message 3\n" + + "[INFO] message 4\n" + + "[WARNING] minor\n" + + "[ERROR] fatal\n" + ); + + expect(filtered.buffer).toBe("" + + "[INFO] message 4\n" + ); + + expect(buglog.buffer).toBe("" + + "[INFO] message 4\n" + + "[WARNING] minor\n" + + "[ERROR] fatal\n" + ); + + context1.buffer = ""; + + }); + }); From 192b12405ea19bca2bcadb3fcd02ec0ec05962c5 Mon Sep 17 00:00:00 2001 From: namark Date: Mon, 22 Nov 2021 18:27:31 +0400 Subject: [PATCH 15/23] Added permanent vircadia-sdk tag to main logger instance. --- src/domain/shared/Log.ts | 4 ++-- tests/domain/shared/Log.unit.test.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index c6e5e31a..04e1ae7e 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -52,7 +52,7 @@ type LoggableRecord = LoggablePrimitive | Record; * @interface Loggable */ interface Loggable { - /*devdoc + /*@devdoc * Converts the object to a loggable representation * @return {LoggableRecord} - a representation of the object based on loggable primitives */ @@ -519,7 +519,7 @@ const DefaultLogConfiguration = { * The Log is the main {@linkcode Logger} instance used in the SDK. * @type {Logger} */ -const Log = new Logger(DefaultLogConfiguration); +const Log = new Logger(DefaultLogConfiguration).tag("vircadia-sdk"); export default Log; export { diff --git a/tests/domain/shared/Log.unit.test.ts b/tests/domain/shared/Log.unit.test.ts index 472def96..5f10b3c6 100644 --- a/tests/domain/shared/Log.unit.test.ts +++ b/tests/domain/shared/Log.unit.test.ts @@ -8,6 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* eslint-disable @typescript-eslint/no-magic-numbers */ + import { Logger, LogLevel, @@ -357,8 +359,9 @@ describe("Logger - unit tests", () => { context: new LoggerContextCombination( context1, context2, - new LogFilterContext(filtered, - (level: LogLevel) => level === LogLevel.INFO), + new LogFilterContext(filtered, (level: LogLevel) => { + return level === LogLevel.INFO; + }), new LogReportContext(buglog, 3) ) }); From 1fe74b1c81c4c79bbfe0ba54b85e66b23038d236 Mon Sep 17 00:00:00 2001 From: namark Date: Mon, 22 Nov 2021 18:51:58 +0400 Subject: [PATCH 16/23] Logger configuration exported, filtering disabled in example app. --- example/interface.js | 11 ++++++++++- src/Vircadia.ts | 2 ++ src/domain/shared/Log.ts | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/example/interface.js b/example/interface.js index 5ed3855b..65b126a5 100644 --- a/example/interface.js +++ b/example/interface.js @@ -8,10 +8,19 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import { Vircadia, DomainServer, AudioMixer, AvatarMixer, MessageMixer, Uuid } from "../dist/Vircadia.js"; +import { + Vircadia, + DefaultLogConfiguration, + DomainServer, AudioMixer, + AvatarMixer, + MessageMixer, + Uuid +} from "../dist/Vircadia.js"; (function () { + DefaultLogConfiguration.filter = undefined; + const DEFAULT_URL = "ws://127.0.0.1:40102"; // Shared context. diff --git a/src/Vircadia.ts b/src/Vircadia.ts index df78976a..0273bc3f 100644 --- a/src/Vircadia.ts +++ b/src/Vircadia.ts @@ -48,3 +48,5 @@ export type { vec3 } from "./domain/shared/Vec3"; export { default as Quat } from "./domain/shared/Quat"; export type { quat } from "./domain/shared/Quat"; + +export { DefaultLogConfiguration } from "./domain/shared/Log"; diff --git a/src/domain/shared/Log.ts b/src/domain/shared/Log.ts index 04e1ae7e..e701aada 100644 --- a/src/domain/shared/Log.ts +++ b/src/domain/shared/Log.ts @@ -510,7 +510,7 @@ class Logger { */ const DefaultLogConfiguration = { context: new ConsoleLoggerContext(), - levelFilter: (level: LogLevel) => { + filter: (level: LogLevel) => { return level >= LogLevel.WARNING; } } as LoggerConfiguration; From 97f7723fe256909fc3c9d2a7dff35040ed65a5a3 Mon Sep 17 00:00:00 2001 From: namark Date: Thu, 25 Nov 2021 23:54:31 +0400 Subject: [PATCH 17/23] Moved/renamed logger and its unit tests. --- src/Vircadia.ts | 2 +- src/domain/audio-client/AudioClient.ts | 2 +- src/domain/audio/InboundAudioStream.ts | 2 +- src/{domain => }/shared/Log.ts | 0 .../Log.unit.test.ts => shared/Log.unit.test.js} | 12 ++++++------ 5 files changed, 9 insertions(+), 9 deletions(-) rename src/{domain => }/shared/Log.ts (100%) rename tests/{domain/shared/Log.unit.test.ts => shared/Log.unit.test.js} (97%) diff --git a/src/Vircadia.ts b/src/Vircadia.ts index 0273bc3f..2a7cbad9 100644 --- a/src/Vircadia.ts +++ b/src/Vircadia.ts @@ -49,4 +49,4 @@ export type { vec3 } from "./domain/shared/Vec3"; export { default as Quat } from "./domain/shared/Quat"; export type { quat } from "./domain/shared/Quat"; -export { DefaultLogConfiguration } from "./domain/shared/Log"; +export { DefaultLogConfiguration } from "./shared/Log"; diff --git a/src/domain/audio-client/AudioClient.ts b/src/domain/audio-client/AudioClient.ts index e110254c..e1c799e3 100644 --- a/src/domain/audio-client/AudioClient.ts +++ b/src/domain/audio-client/AudioClient.ts @@ -22,7 +22,7 @@ import PacketScribe from "../networking/packets/PacketScribe"; import PacketType, { PacketTypeValue } from "../networking/udt/PacketHeaders"; import assert from "../shared/assert"; import ContextManager from "../shared/ContextManager"; -import Log from "../shared/Log"; +import Log from "../../shared/Log"; import Vec3, { vec3 } from "../shared/Vec3"; diff --git a/src/domain/audio/InboundAudioStream.ts b/src/domain/audio/InboundAudioStream.ts index bbeddad2..9b1890f9 100644 --- a/src/domain/audio/InboundAudioStream.ts +++ b/src/domain/audio/InboundAudioStream.ts @@ -16,7 +16,7 @@ import { SilentAudioFrameDetails } from "../networking/packets/SilentAudioFrame" import PacketType from "../networking/udt/PacketHeaders"; import UDT from "../networking/udt/UDT"; import ContextManager from "../shared/ContextManager"; -import Log from "../shared/Log"; +import Log from "../../shared/Log"; /*@devdoc diff --git a/src/domain/shared/Log.ts b/src/shared/Log.ts similarity index 100% rename from src/domain/shared/Log.ts rename to src/shared/Log.ts diff --git a/tests/domain/shared/Log.unit.test.ts b/tests/shared/Log.unit.test.js similarity index 97% rename from tests/domain/shared/Log.unit.test.ts rename to tests/shared/Log.unit.test.js index 5f10b3c6..f739c163 100644 --- a/tests/domain/shared/Log.unit.test.ts +++ b/tests/shared/Log.unit.test.js @@ -19,7 +19,7 @@ import { LoggerContextCombination, LogFilterContext, LogReportContext -} from "../../../src/domain/shared/Log"; +} from "../../src/shared/Log"; describe("Logger - unit tests", () => { @@ -107,7 +107,7 @@ describe("Logger - unit tests", () => { test("Filtering by level", () => { const context = new StringLoggerContext(); - const config = { context } as LoggerConfiguration; + const config = { context }; const logger = new Logger(config); config.levelFilter = (level) => { @@ -162,7 +162,7 @@ describe("Logger - unit tests", () => { test("Filtering by tags", () => { const context = new StringLoggerContext(); - const config = { context } as LoggerConfiguration; + const config = { context }; const logger = new Logger(config); config.tagFilter = ["Type 2", "Type 4"]; @@ -233,7 +233,7 @@ describe("Logger - unit tests", () => { test("Mixed filtering", () => { const context = new StringLoggerContext(); - const config = { context } as LoggerConfiguration; + const config = { context }; const logger = new Logger(config); config.levelFilter = (level) => { @@ -256,7 +256,7 @@ describe("Logger - unit tests", () => { test("Log a message only once", () => { const context = new StringLoggerContext(); - const config = { context } as LoggerConfiguration; + const config = { context }; const logger = new Logger(config); logger.one.debug("message"); @@ -321,7 +321,7 @@ describe("Logger - unit tests", () => { test("Multiple tags", () => { const context = new StringLoggerContext(); - const config = { context } as LoggerConfiguration; + const config = { context }; const logger = new Logger(config).tag("toplevel"); logger.tag("one", "two").message("message"); From d85dcdf05e05ad61f3005832467cd648f2269870 Mon Sep 17 00:00:00 2001 From: namark Date: Fri, 26 Nov 2021 02:08:17 +0400 Subject: [PATCH 18/23] Added unit tests for the main logger instance. --- tests/shared/Log.unit.test.js | 100 +++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/tests/shared/Log.unit.test.js b/tests/shared/Log.unit.test.js index f739c163..912044c5 100644 --- a/tests/shared/Log.unit.test.js +++ b/tests/shared/Log.unit.test.js @@ -10,15 +10,15 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import { +import Log, { Logger, LogLevel, - LoggerConfiguration, ConsoleLoggerContext, StringLoggerContext, LoggerContextCombination, LogFilterContext, - LogReportContext + LogReportContext, + DefaultLogConfiguration } from "../../src/shared/Log"; describe("Logger - unit tests", () => { @@ -443,4 +443,98 @@ describe("Logger - unit tests", () => { }); + test("Default behavior and configuration of the main SDK Logger instance", () => { + + const debug = jest.spyOn(console, "debug").mockImplementation(() => { /* no-op */ }); + const log = jest.spyOn(console, "log").mockImplementation(() => { /* no-op */ }); + const info = jest.spyOn(console, "info").mockImplementation(() => { /* no-op */ }); + const warn = jest.spyOn(console, "warn").mockImplementation(() => { /* no-op */ }); + const error = jest.spyOn(console, "error").mockImplementation(() => { /* no-op */ }); + + Log.debug("Console debug message"); + Log.message("Console log message"); + Log.info("Console info message"); + Log.warning("Console warning message"); + Log.error("Console error message"); + + expect(warn).toHaveBeenCalledWith("[vircadia-sdk]", "Console warning message"); + expect(error).toHaveBeenCalledWith("[vircadia-sdk]", "Console error message"); + + debug.mockClear(); + log.mockClear(); + info.mockClear(); + warn.mockClear(); + error.mockClear(); + + Log.tag("Type 1").debug("Console debug message"); + Log.tag("Type 2").message("Console log message"); + Log.tag("Type 3").info("Console info message"); + Log.tag("Type 4").warning("Console warning message"); + Log.tag("Type 5").error("Console error message"); + + expect(warn).toHaveBeenCalledWith("[vircadia-sdk][Type 4]", "Console warning message"); + expect(error).toHaveBeenCalledWith("[vircadia-sdk][Type 5]", "Console error message"); + + debug.mockClear(); + log.mockClear(); + info.mockClear(); + warn.mockClear(); + error.mockClear(); + + DefaultLogConfiguration.filter = undefined; + + Log.debug("Console debug message"); + Log.message("Console log message"); + Log.info("Console info message"); + Log.warning("Console warning message"); + Log.error("Console error message"); + + expect(debug).toHaveBeenCalledWith("[vircadia-sdk]", "Console debug message"); + expect(log).toHaveBeenCalledWith("[vircadia-sdk]", "Console log message"); + expect(info).toHaveBeenCalledWith("[vircadia-sdk]", "Console info message"); + expect(warn).toHaveBeenCalledWith("[vircadia-sdk]", "Console warning message"); + expect(error).toHaveBeenCalledWith("[vircadia-sdk]", "Console error message"); + + debug.mockClear(); + log.mockClear(); + info.mockClear(); + warn.mockClear(); + error.mockClear(); + + Log.tag("Type 1").debug("Console debug message"); + Log.tag("Type 2").message("Console log message"); + Log.tag("Type 3").info("Console info message"); + Log.tag("Type 4").warning("Console warning message"); + Log.tag("Type 5").error("Console error message"); + + expect(debug).toHaveBeenCalledWith("[vircadia-sdk][Type 1]", "Console debug message"); + expect(log).toHaveBeenCalledWith("[vircadia-sdk][Type 2]", "Console log message"); + expect(info).toHaveBeenCalledWith("[vircadia-sdk][Type 3]", "Console info message"); + expect(warn).toHaveBeenCalledWith("[vircadia-sdk][Type 4]", "Console warning message"); + expect(error).toHaveBeenCalledWith("[vircadia-sdk][Type 5]", "Console error message"); + + debug.mockRestore(); + log.mockRestore(); + info.mockRestore(); + warn.mockRestore(); + error.mockRestore(); + + DefaultLogConfiguration.context = new StringLoggerContext(); + + Log.tag("Type 1").debug("String debug message"); + Log.tag("Type 2").message("String log message"); + Log.tag("Type 3").info("String info message"); + Log.tag("Type 4").warning("String warning message"); + Log.tag("Type 5").error("String error message"); + + expect(DefaultLogConfiguration.context.buffer).toBe("" + + "[vircadia-sdk][Type 1][DEBUG] String debug message\n" + + "[vircadia-sdk][Type 2][DEFAULT] String log message\n" + + "[vircadia-sdk][Type 3][INFO] String info message\n" + + "[vircadia-sdk][Type 4][WARNING] String warning message\n" + + "[vircadia-sdk][Type 5][ERROR] String error message\n" + ); + + }); + }); From ebb5683f60c8548e8bbb4cfd8554c5fccde7abbd Mon Sep 17 00:00:00 2001 From: namark Date: Fri, 26 Nov 2021 20:23:57 +0400 Subject: [PATCH 19/23] Fixed centralized logger documentation. --- src/shared/Log.ts | 209 ++++++++++++++++++++++------------------------ 1 file changed, 100 insertions(+), 109 deletions(-) diff --git a/src/shared/Log.ts b/src/shared/Log.ts index e701aada..53681c92 100644 --- a/src/shared/Log.ts +++ b/src/shared/Log.ts @@ -37,110 +37,31 @@ const AllLogLevels = [ LogLevel.ERROR ] as const; -/*@devdoc - * @typedef {string | symbol | number | bigint | boolean | undefined | null} LoggablePrimitive - */ type LoggablePrimitive = string | symbol | number | bigint | boolean | undefined | null; -/*@devdoc - * @typedef {LoggablePrimitive | Record} LoggableRecord - */ type LoggableRecord = LoggablePrimitive | Record; -/*@devdoc - * Implementing this interface will allow passing the objects of the class to the {@linkcode Logger} - * @interface Loggable - */ interface Loggable { - /*@devdoc - * Converts the object to a loggable representation - * @return {LoggableRecord} - a representation of the object based on loggable primitives - */ toLoggable(): LoggableRecord; } -/*@devdoc - * @typedef {LoggablePrimitive | Loggable} LoggableObject - */ type LoggableObject = LoggablePrimitive | Loggable; -/*@devdoc - * @typedef {LoggablePrimitive | Loggable} LoggableObjectRecord - */ type LoggableObjectRecord = LoggableObject | Record; -/*@devdoc - * A predicate for filtering log messages - * @callback LogMessagePredicate - * @param {LogLevel} level - The logging level of the message. - * @param {Set} tags - The tags associated with the message. - * @return {...LoggableRecord} loggables - the objects that comprise the message body - * @return {boolean} - Whether to include/allow the message. - */ type LogMessagePredicate = (level: LogLevel, tags: Set, ...loggables: LoggableRecord[]) => boolean; -/*@devdoc - * Predicate function for filtering log messages based on log level. - * @callback LogLevelPredicate - * @param {LogLevel} level - The level in question. - * @return {boolean} Whether to show messages of the specified level. - */ - -/*@devdoc - * The LoggerContext is an interface used in {@linkcode LoggerConfiguration} - * to specify the output of the {@linkcode Logger} class. - * @interface LoggerContext - */ interface LoggerContext { - /*@devdoc - * Outputs the log message. - * @function - * @name LoggerContext#log - * @param {LogLevel} level - The level to log the message at. - * @param {Set} tags - The tags associated with the message. - * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. - */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void; } -/*@devdoc - * The LoggerConfiguration is an interface for configuration of the {@linkcode Logger} class. - * @interface LoggerConfiguration - */ type LoggerConfiguration = { - /*@devdoc - * A full message filter that is applied to log input, before it is passed to the context. - * @type {LogMessagePredicate} - * @name [LoggerConfiguration#filter] - */ + context: LoggerContext; filter?: LogMessagePredicate; - - - /*@devdoc - * A level filter that is applied to log input, before the {@linkcode LoggerConfiguration#filter} - * @type {LogLevelPredicate} - * @name [LoggerConfiguration#levelFilter] - */ levelFilter?: (level: LogLevel) => boolean; - - - /*@devdoc - * A log message tag specific filter that is applied to log input, before the {@linkcode LoggerConfiguration#filter} - * A given message will be included if it has any of the specified tags. - * undefined value in the array specifies to include messages without any tags. - * @type {Array} - * @name [LoggerConfiguration#tagFilter] - */ tagFilter?: Array; - /*@devdoc - * The context that specifies the logger output. - * @type {LoggerContext} - * @name LoggerConfiguration#context - */ - context: LoggerContext; - }; function loggablePrimitiveToString(loggable: LoggablePrimitive): string | undefined { @@ -186,19 +107,45 @@ function loggableToString(loggable: LoggableRecord) { || loggableRecordToString(loggable as Record); } -/*@devdoc +/*@sdkdoc * The ConsoleLoggerContext is a logger output configuration for logging to the dev console. * * @class ConsoleLoggerContext * @implements LoggerContext */ class ConsoleLoggerContext implements LoggerContext { + + /*@sdkdoc + * The basic types that should be possible to log. + * @typedef {string | symbol | number | bigint | boolean | undefined | null} LoggablePrimitive + */ + + /*@sdkdoc + * The message body structure that every implementation of {@linkcode LoggerContext} must support logging. + * @typedef {LoggablePrimitive | Record} LoggableRecord + */ + + /*@sdkdoc + * The LoggerContext is an interface used in {@linkcode LoggerConfiguration} + * to specify the output of the {@linkcode Logger} class. + * @interface LoggerContext + */ + + /*@sdkdoc + * Outputs the log message. + * @function + * @name LoggerContext#log + * @param {LogLevel} level - The level to log the message at. + * @param {Set} tags - The tags associated with the message. + * @param {...LoggableRecord} loggables - A list of objects that comprise the message body. + */ + /* eslint-disable class-methods-use-this */ - /*@devdoc + /*@sdkdoc * Outputs the log to the developer console. * @param {LogLevel} level - The level to log the message at. * @param {Set} tags - The tags associated with the message. - * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + * @param {...LoggableRecord} loggables - A list of objects that comprise the message body. */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { const logFunction = [console.debug, console.log, console.info, console.warn, console.error].at(level); @@ -213,7 +160,7 @@ class ConsoleLoggerContext implements LoggerContext { /* eslint-enable class-methods-use-this */ } -/*@devdoc +/*@sdkdoc * The StringLoggerContext is a output configuration for logging to a string. * * @class StringLoggerContext @@ -224,11 +171,11 @@ class StringLoggerContext implements LoggerContext { buffer = ""; - /*@devdoc + /*@sdkdoc * Outputs the log to the string buffer. * @param {LogLevel} level - The level to log the message at. * @param {Set} tags - The tags associated with the message. - * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + * @param {...LoggableRecord} loggables - A list of objects that comprise the message body. */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { if (tags.size !== 0) { @@ -240,7 +187,7 @@ class StringLoggerContext implements LoggerContext { } -/*@devdoc +/*@sdkdoc * The LogFilterContext is a logger context wrapper for filtering messages. * * @class LogFilterContext @@ -250,6 +197,15 @@ class StringLoggerContext implements LoggerContext { */ class LogFilterContext implements LoggerContext { + /*@sdkdoc + * A predicate for filtering log messages. + * @callback LogMessagePredicate + * @param {LogLevel} level - The logging level of the message. + * @param {Set} tags - The tags associated with the message. + * @param {...LoggableRecord} loggables - A list of objects that comprise the message body. + * @return {boolean} - Whether to include/allow the message. + */ + #_context: LoggerContext; #_filter: LogMessagePredicate; @@ -262,7 +218,7 @@ class LogFilterContext implements LoggerContext { * Passes logs that satisfy the specified filter to the specified context. * @param {LogLevel} level - The level to log the message at. * @param {Set} tags - The tags associated with the message. - * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + * @param {...LoggableRecord} loggables - A list of objects that comprise the message body. */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { if (this.#_filter(level, tags, ...loggables)) { @@ -271,7 +227,7 @@ class LogFilterContext implements LoggerContext { } } -/*@devdoc +/*@sdkdoc * The LoggerContextCombination combines several logger contexts into one. * * @class LoggerContextCombination @@ -286,11 +242,11 @@ class LoggerContextCombination implements LoggerContext { this.#_contexts = contexts; } - /*@devdoc + /*@sdkdoc * Passes log message to all specified contexts. * @param {LogLevel} level - The level to log the message at. * @param {Set} tags - The tags associated with the message. - * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + * @param {...LoggableRecord} loggables - A list of objects that comprise the message body. */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { for (const context of this.#_contexts) { @@ -299,7 +255,7 @@ class LoggerContextCombination implements LoggerContext { } } -/*@devdoc +/*@sdkdoc * The LogReportContext buffers log messages and passes them to specified context * when a message is logged at the ERROR level or above. * @@ -320,11 +276,11 @@ class LogReportContext implements LoggerContext { this.#_logCount = logCount; } - /*@devdoc + /*@sdkdoc * Buffers the logs and passes them to the specified context. * @param {LogLevel} level - The level to log the message at. * @param {Set} tags - The tags associated with the message. - * @param {LoggableObject[]} loggables - (@linkcode LoggableObject) list that comprises the message body. + * @param {...LoggableRecord} loggables - A list of objects that comprise the message body. */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { @@ -357,6 +313,50 @@ class LogReportContext implements LoggerContext { */ class Logger { + /*@devdoc + * Implementing this interface will allow passing the objects of the class to the {@linkcode Logger}. + * @interface Loggable + */ + + /*@devdoc + * Converts the object to a loggable representation. + * @function + * @name Loggable#toLoggable + * @return {LoggableRecord} - a representation of the object based on loggable primitives. + */ + + /*@devdoc + * Basic type of objects that can be passed to the {@linkcode Logger} for logging. + * @typedef {LoggablePrimitive | Loggable} LoggableObject + */ + + /*@devdoc + * The message body structure that {@linkcode Logger} supports. + * + * @typedef {LoggableObject | Record} LoggableObjectRecord + */ + + /*@devdoc + * Predicate function for filtering log messages based on log level. + * @callback LogLevelPredicate + * @param {LogLevel} level - The level in question. + * @return {boolean} Whether to show messages of the given level. + */ + + /*@sdkdoc + * The LoggerConfiguration is an interface for configuration of the {@linkcode Logger} class. + * @typedef {object} LoggerConfiguration + * @property {LoggerContext} context - The context that specifies the logger output. + * @property {LogMessagePredicate} [filter] - A full message filter that is applied to log input, + * before it is passed to the context. + * @property {LogLevelPredicate} [levelFilter] - A level filter that is applied to log input, + * before the {@linkcode LoggerConfiguration#filter}. + * @property {Array} [tagFilter] - A tag specific filter that is applied to log input, + * before the {@linkcode LoggerConfiguration#filter}. + * A given message will be included if it has any of the specified tags. + * undefined value in the array specifies to include messages without any tags. + */ + readonly DEBUG = LogLevel.DEBUG; readonly DEFAULT = LogLevel.DEFAULT; readonly INFO = LogLevel.INFO; @@ -414,14 +414,13 @@ class Logger { level(level: LogLevel, ...loggableObjects: LoggableObjectRecord[]): void { const loggables = loggableObjects.map((obj) => { const loggable = obj as Loggable; - if (loggable && loggable.toLoggable) { + if (loggable && typeof loggable.toLoggable === "function") { return loggable.toLoggable(); } return obj as LoggableRecord; }); if (this.#_once) { - const id = `${this.#_tagsKey} | ${level} | ${loggables.map(loggableToString).join(" | ")}`; if (!this.#_messageFlags.get(id)) { @@ -495,18 +494,9 @@ class Logger { } /*@sdkdoc - * The DefaultLogConfiguration is the main {@linkcode LoggerConfiguration} object used by the SDK. - * - * @namespace DefaultLogConfiguration - * @property {LoggerContext} context - The context that specifies the output destination for the logs. - * @property {LogMessagePredicate} [filter] - A full message filter that is applied to log input, - * before it is passed to the context. - * @property {LogLevelPredicate} [levelFilter] - A level filter that is applied to log input, - * before the {@linkcode DefaultLogConfiguration#filter} - * @property {Array} [tagFilter] - A log message tag specific filter that is applied to log input, - * before the {@linkcode DefaultLogConfiguration#filter}. - * A given message will be included if it has any of the specified tags. - * undefined value in the array specifies to include messages without any tags. + * The DefaultLogConfiguration allows the customization of the SDK logger output. + * By default the context is {@linkcode ConsoleLoggerContext}, and filter is set to allow warning and error messages only. + * @type {LoggerConfiguration} */ const DefaultLogConfiguration = { context: new ConsoleLoggerContext(), @@ -516,7 +506,8 @@ const DefaultLogConfiguration = { } as LoggerConfiguration; /*@devdoc - * The Log is the main {@linkcode Logger} instance used in the SDK. + * The Log is the main {@linkcode Logger} instance used in the SDK, + * initialized with {@linkcode DefaultLogConfiguration} and a "vircadia-sdk" tag. * @type {Logger} */ const Log = new Logger(DefaultLogConfiguration).tag("vircadia-sdk"); From e1c3b3869ab840c656077ec7d85f08e68429bcc4 Mon Sep 17 00:00:00 2001 From: namark Date: Fri, 26 Nov 2021 20:24:40 +0400 Subject: [PATCH 20/23] Unit test for custom loggable interface. --- tests/shared/Log.unit.test.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/shared/Log.unit.test.js b/tests/shared/Log.unit.test.js index 912044c5..ad83b14a 100644 --- a/tests/shared/Log.unit.test.js +++ b/tests/shared/Log.unit.test.js @@ -359,7 +359,7 @@ describe("Logger - unit tests", () => { context: new LoggerContextCombination( context1, context2, - new LogFilterContext(filtered, (level: LogLevel) => { + new LogFilterContext(filtered, (level) => { return level === LogLevel.INFO; }), new LogReportContext(buglog, 3) @@ -537,4 +537,35 @@ describe("Logger - unit tests", () => { }); + test("Custom loggable object", () => { + + const customLoggable = new class { + toLoggable() { + return "this is a custom loggable"; + } + }; + + const context = new StringLoggerContext(); + const logger = new Logger({ context }); + + logger.message(customLoggable); + + expect(context.buffer).toBe("" + + "[DEFAULT] this is a custom loggable\n" + ); + context.buffer = ""; + + logger.message({ + toLoggable: "but this is not" + }); + + expect(context.buffer).toBe("" + + "[DEFAULT] \n" + + " toLoggable: but this is not\n" + + "\n" + ); + context.buffer = ""; + + }); + }); From 4731f2af232b2af2e31b8b69303cd8b5d137f2a4 Mon Sep 17 00:00:00 2001 From: namark Date: Fri, 26 Nov 2021 20:39:48 +0400 Subject: [PATCH 21/23] Exposing the various logger contexts to the SDK user. --- src/Vircadia.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Vircadia.ts b/src/Vircadia.ts index 2a7cbad9..da169045 100644 --- a/src/Vircadia.ts +++ b/src/Vircadia.ts @@ -49,4 +49,10 @@ export type { vec3 } from "./domain/shared/Vec3"; export { default as Quat } from "./domain/shared/Quat"; export type { quat } from "./domain/shared/Quat"; -export { DefaultLogConfiguration } from "./shared/Log"; +export { + DefaultLogConfiguration, + ConsoleLoggerContext, + LogFilterContext, + LoggerContextCombination, + LogReportContext +} from "./shared/Log"; From bebc2345cd8993c5b430ece9a1a97612ed28b3eb Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 27 Nov 2021 02:46:21 +0400 Subject: [PATCH 22/23] Fixed linting errors in log unit test. --- tests/shared/Log.unit.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/shared/Log.unit.test.js b/tests/shared/Log.unit.test.js index ad83b14a..b99ebe25 100644 --- a/tests/shared/Log.unit.test.js +++ b/tests/shared/Log.unit.test.js @@ -540,10 +540,11 @@ describe("Logger - unit tests", () => { test("Custom loggable object", () => { const customLoggable = new class { + // eslint-disable-next-line class-methods-use-this toLoggable() { return "this is a custom loggable"; } - }; + }(); const context = new StringLoggerContext(); const logger = new Logger({ context }); From 5624eb54f53c87b4068bdb8ef1b5170a0c0085fc Mon Sep 17 00:00:00 2001 From: namark Date: Sat, 27 Nov 2021 02:50:10 +0400 Subject: [PATCH 23/23] Replaced usage of 'at' function with subscript operator. --- src/shared/Log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/Log.ts b/src/shared/Log.ts index 53681c92..5e71c54e 100644 --- a/src/shared/Log.ts +++ b/src/shared/Log.ts @@ -148,7 +148,7 @@ class ConsoleLoggerContext implements LoggerContext { * @param {...LoggableRecord} loggables - A list of objects that comprise the message body. */ log(level: LogLevel, tags: Set, ...loggables: LoggableRecord[]): void { - const logFunction = [console.debug, console.log, console.info, console.warn, console.error].at(level); + const logFunction = [console.debug, console.log, console.info, console.warn, console.error][level]; if (logFunction) { if (tags.size === 0) { logFunction(...loggables);