From 08d8ee0df51a7e61a3932c3489d50a13fbff786e Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Fri, 31 Jan 2025 09:57:51 +0100 Subject: [PATCH] refactor: pluggable LogContainer --- packages/config/src/ConfigManager.ts | 8 +- packages/config/src/Logger.ts | 4 +- packages/core/src/bindings/log/node.ts | 281 +++++++++++++++++ packages/core/src/index.ts | 5 +- packages/core/src/index_browser.ts | 10 +- packages/core/src/log/Controller.ts | 4 +- packages/core/src/log/ZWaveLoggerBase.ts | 4 +- packages/core/src/log/shared.ts | 292 +----------------- packages/core/src/log/shared_safe.ts | 25 ++ packages/core/src/log/traits.ts | 21 +- .../src/rules/no-forbidden-imports.ts | 1 + packages/serial/src/log/Logger.ts | 4 +- packages/serial/src/mock/MockPort.ts | 4 +- .../src/serialport/ZWaveSerialStream.ts | 5 +- .../serial/src/zniffer/ZnifferSerialStream.ts | 5 +- packages/zwave-js/src/lib/driver/Driver.ts | 50 +-- .../zwave-js/src/lib/driver/ZWaveOptions.ts | 6 + packages/zwave-js/src/lib/log/Driver.ts | 4 +- packages/zwave-js/src/lib/log/Zniffer.ts | 4 +- packages/zwave-js/src/lib/zniffer/Zniffer.ts | 20 +- 20 files changed, 399 insertions(+), 358 deletions(-) create mode 100644 packages/core/src/bindings/log/node.ts diff --git a/packages/config/src/ConfigManager.ts b/packages/config/src/ConfigManager.ts index c5c0a4386130..0a104c49f650 100644 --- a/packages/config/src/ConfigManager.ts +++ b/packages/config/src/ConfigManager.ts @@ -1,14 +1,14 @@ import { + type LogContainer, ZWaveError, ZWaveErrorCodes, - ZWaveLogContainer, isZWaveError, } from "@zwave-js/core"; +import { log as createZWaveLogContainer } from "@zwave-js/core/bindings/log/node"; import { getErrorMessage, pathExists } from "@zwave-js/shared"; import { type FileSystem } from "@zwave-js/shared/bindings"; import path from "pathe"; import { ConfigLogger } from "./Logger.js"; -import { type ConfigLogContext } from "./Logger_safe.js"; import { type ManufacturersMap, loadManufacturersInternal, @@ -35,7 +35,7 @@ import { export interface ConfigManagerOptions { bindings?: FileSystem; - logContainer?: ZWaveLogContainer; + logContainer?: LogContainer; deviceConfigPriorityDir?: string; deviceConfigExternalDir?: string; } @@ -44,7 +44,7 @@ export class ConfigManager { public constructor(options: ConfigManagerOptions = {}) { this._fs = options.bindings; this.logger = new ConfigLogger( - options.logContainer ?? new ZWaveLogContainer({ enabled: false }), + options.logContainer ?? createZWaveLogContainer({ enabled: false }), ); this.deviceConfigPriorityDir = options.deviceConfigPriorityDir; this.deviceConfigExternalDir = options.deviceConfigExternalDir; diff --git a/packages/config/src/Logger.ts b/packages/config/src/Logger.ts index 7c8d26d0ef5d..a258bd37d6f6 100644 --- a/packages/config/src/Logger.ts +++ b/packages/config/src/Logger.ts @@ -1,5 +1,5 @@ import { - type ZWaveLogContainer, + type LogContainer, ZWaveLoggerBase, getDirectionPrefix, } from "@zwave-js/core"; @@ -10,7 +10,7 @@ import { } from "./Logger_safe.js"; export class ConfigLogger extends ZWaveLoggerBase { - constructor(loggers: ZWaveLogContainer) { + constructor(loggers: LogContainer) { super(loggers, CONFIG_LABEL); } diff --git a/packages/core/src/bindings/log/node.ts b/packages/core/src/bindings/log/node.ts new file mode 100644 index 000000000000..5adf33248ee9 --- /dev/null +++ b/packages/core/src/bindings/log/node.ts @@ -0,0 +1,281 @@ +import path from "pathe"; +import { configs } from "triple-beam"; +import winston from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; +import type Transport from "winston-transport"; +import type { ConsoleTransportInstance } from "winston/lib/winston/transports"; +import { + createDefaultTransportFormat, + createLoggerFormat, +} from "../../log/shared.js"; +import { + type LogConfig, + type LogContext, + type LogFactory, + nonUndefinedLogConfigKeys, + stringToNodeList, +} from "../../log/shared_safe.js"; +import { type LogContainer, type ZWaveLogger } from "../../log/traits.js"; + +const isTTY = process.stdout.isTTY; +const isUnitTest = process.env.NODE_ENV === "test"; + +const loglevels = configs.npm.levels; + +function getTransportLoglevel(): string { + return process.env.LOGLEVEL! in loglevels ? process.env.LOGLEVEL! : "debug"; +} + +/** Performs a reverse lookup of the numeric loglevel */ +function loglevelFromNumber(numLevel: number | undefined): string | undefined { + if (numLevel == undefined) return; + for (const [level, value] of Object.entries(loglevels)) { + if (value === numLevel) return level; + } +} + +class ZWaveLogContainer extends winston.Container + implements LogContainer +{ + private fileTransport: DailyRotateFile | undefined; + private consoleTransport: ConsoleTransportInstance | undefined; + private loglevelVisibleCache = new Map(); + + private logConfig: LogConfig & { level: string } = { + enabled: true, + level: getTransportLoglevel(), + logToFile: !!process.env.LOGTOFILE, + maxFiles: 7, + nodeFilter: stringToNodeList(process.env.LOG_NODES), + transports: undefined as any, + filename: path.join(process.cwd(), `zwavejs_%DATE%.log`), + forceConsole: false, + }; + + constructor(config: Partial = {}) { + super(); + this.updateConfiguration(config); + } + + public getLogger(label: string): ZWaveLogger { + if (!this.has(label)) { + this.add(label, { + transports: this.getAllTransports(), + format: createLoggerFormat(label), + // Accept all logs, no matter what. The individual loggers take care + // of filtering the wrong loglevels + level: "silly", + }); + } + + return this.get(label) as unknown as ZWaveLogger; + } + + public updateConfiguration(config: Partial): void { + // Avoid overwriting configuration settings with undefined if they shouldn't be + for (const key of nonUndefinedLogConfigKeys) { + if (key in config && config[key] === undefined) { + delete config[key]; + } + } + const changedLoggingTarget = (config.logToFile != undefined + && config.logToFile !== this.logConfig.logToFile) + || (config.forceConsole != undefined + && config.forceConsole !== this.logConfig.forceConsole); + + if (typeof config.level === "number") { + config.level = loglevelFromNumber(config.level); + } + const changedLogLevel = config.level != undefined + && config.level !== this.logConfig.level; + + if ( + config.filename != undefined + && !config.filename.includes("%DATE%") + ) { + config.filename += "_%DATE%.log"; + } + const changedFilename = config.filename != undefined + && config.filename !== this.logConfig.filename; + + if (config.maxFiles != undefined) { + if ( + typeof config.maxFiles !== "number" + || config.maxFiles < 1 + || config.maxFiles > 365 + ) { + delete config.maxFiles; + } + } + const changedMaxFiles = config.maxFiles != undefined + && config.maxFiles !== this.logConfig.maxFiles; + + this.logConfig = Object.assign(this.logConfig, config); + + // If the loglevel changed, our cached "is visible" info is out of date + if (changedLogLevel) { + this.loglevelVisibleCache.clear(); + } + + // When the log target (console, file, filename) was changed, recreate the internal transports + // because at least the filename does not update dynamically + // Also do this when configuring the logger for the first time + const recreateInternalTransports = (this.fileTransport == undefined + && this.consoleTransport == undefined) + || changedLoggingTarget + || changedFilename + || changedMaxFiles; + + if (recreateInternalTransports) { + this.fileTransport?.destroy(); + this.fileTransport = undefined; + this.consoleTransport?.destroy(); + this.consoleTransport = undefined; + } + + // When the internal transports or the custom transports were changed, we need to update the loggers + if (recreateInternalTransports || config.transports != undefined) { + this.loggers.forEach((logger) => + logger.configure({ transports: this.getAllTransports() }) + ); + } + } + + public getConfiguration(): LogConfig { + return this.logConfig; + } + + /** Tests whether a log using the given loglevel will be logged */ + public isLoglevelVisible(loglevel: string): boolean { + // If we are not connected to a TTY, not logging to a file and don't have any custom transports, we won't see anything + if ( + !this.fileTransport + && !this.consoleTransport + && (!this.logConfig.transports + || this.logConfig.transports.length === 0) + ) { + return false; + } + + if (!this.loglevelVisibleCache.has(loglevel)) { + this.loglevelVisibleCache.set( + loglevel, + loglevel in loglevels + && loglevels[loglevel] <= loglevels[this.logConfig.level], + ); + } + return this.loglevelVisibleCache.get(loglevel)!; + } + + public destroy(): void { + for (const key in this.loggers) { + this.close(key); + } + + this.fileTransport = undefined; + this.consoleTransport = undefined; + this.logConfig.transports = []; + } + + private getAllTransports(): Transport[] { + return [ + ...this.getInternalTransports(), + ...(this.logConfig.transports ?? []), + ]; + } + + private getInternalTransports(): Transport[] { + const ret: Transport[] = []; + + // If logging is disabled, don't log to any of the default transports + if (!this.logConfig.enabled) { + return ret; + } + + // Log to file only when opted in + if (this.logConfig.logToFile) { + if (!this.fileTransport) { + this.fileTransport = this.createFileTransport(); + } + ret.push(this.fileTransport); + } + + // Console logs can be noise, so only log to console... + if ( + // when in production + !isUnitTest + // and stdout is a TTY while we're not already logging to a file + && ((isTTY && !this.logConfig.logToFile) + // except when the user explicitly wants to + || this.logConfig.forceConsole) + ) { + if (!this.consoleTransport) { + this.consoleTransport = this.createConsoleTransport(); + } + ret.push(this.consoleTransport); + } + + return ret; + } + + private createConsoleTransport(): ConsoleTransportInstance { + return new winston.transports.Console({ + format: createDefaultTransportFormat( + // Only colorize the output if logging to a TTY, otherwise we'll get + // ansi color codes in logfiles or redirected shells + isTTY || isUnitTest, + // Only use short timestamps if logging to a TTY + isTTY, + ), + silent: this.isConsoleTransportSilent(), + }); + } + + private isConsoleTransportSilent(): boolean { + return process.env.NODE_ENV === "test" || !this.logConfig.enabled; + } + + private isFileTransportSilent(): boolean { + return !this.logConfig.enabled; + } + + private createFileTransport(): DailyRotateFile { + const ret = new DailyRotateFile({ + filename: this.logConfig.filename, + auditFile: `${ + this.logConfig.filename + .replace("_%DATE%", "_logrotate") + .replace(/\.log$/, "") + }.json`, + datePattern: "YYYY-MM-DD", + createSymlink: true, + symlinkName: path + .basename(this.logConfig.filename) + .replace(`_%DATE%`, "_current"), + zippedArchive: true, + maxFiles: `${this.logConfig.maxFiles}d`, + format: createDefaultTransportFormat(false, false), + silent: this.isFileTransportSilent(), + }); + ret.on("new", (newFilename: string) => { + console.log(`Logging to file: + ${newFilename}`); + }); + ret.on("error", (err: Error) => { + console.error(`Error in file stream rotator: ${err.message}`); + }); + return ret; + } + + /** + * Checks the log configuration whether logs should be written for a given node id + */ + public isNodeLoggingVisible(nodeId: number): boolean { + // If no filters are set, every node gets logged + if (!this.logConfig.nodeFilter) return true; + return this.logConfig.nodeFilter.includes(nodeId); + } +} + +export const log: LogFactory = (config?: Partial) => + new ZWaveLogContainer(config); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 108793f84a73..97fe958299c5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/consistent-type-exports */ export * from "./crypto/index.js"; export * from "./definitions/index.js"; export * from "./dsk/index.js"; @@ -19,7 +18,7 @@ export * from "./security/Manager2Types.js"; export * from "./security/ctr_drbg.js"; export * from "./test/assertZWaveError.js"; export type * from "./traits/index.js"; -export * from "./util/_Types.js"; +export type * from "./util/_Types.js"; export * from "./util/compareVersions.js"; export { deflateSync, gunzipSync } from "./util/compression.js"; export * from "./util/config.js"; @@ -35,4 +34,4 @@ export * from "./values/Metadata.js"; export * from "./values/Primitive.js"; export * from "./values/Timeout.js"; export * from "./values/ValueDB.js"; -export * from "./values/_Types.js"; +export type * from "./values/_Types.js"; diff --git a/packages/core/src/index_browser.ts b/packages/core/src/index_browser.ts index a5843595db1a..7c3e141c390b 100644 --- a/packages/core/src/index_browser.ts +++ b/packages/core/src/index_browser.ts @@ -6,19 +6,17 @@ export * from "./dsk/index.js"; export * from "./error/ZWaveError.js"; export * from "./fsm/FSM.js"; export * from "./log/Controller.definitions.js"; +export * from "./log/Controller.js"; export * from "./log/ZWaveLoggerBase.js"; export * from "./log/shared_safe.js"; export type * from "./log/traits.js"; export * from "./qr/index.js"; -export * from "./registries/DeviceClasses.js"; -export * from "./registries/Indicators.js"; -export * from "./registries/Meters.js"; -export * from "./registries/Notifications.js"; -export * from "./registries/Scales.js"; -export * from "./registries/Sensors.js"; +export * from "./reflection/decorators.js"; +export * from "./registries/index.js"; export * from "./security/Manager.js"; export * from "./security/Manager2.js"; export * from "./security/Manager2Types.js"; +export * from "./security/ctr_drbg.js"; export type * from "./traits/index.js"; export type * from "./util/_Types.js"; export * from "./util/compareVersions.js"; diff --git a/packages/core/src/log/Controller.ts b/packages/core/src/log/Controller.ts index 9a5331d2d5c7..5943211fdb9c 100644 --- a/packages/core/src/log/Controller.ts +++ b/packages/core/src/log/Controller.ts @@ -21,14 +21,14 @@ import { VALUE_LOGLEVEL, } from "./Controller.definitions.js"; import { ZWaveLoggerBase } from "./ZWaveLoggerBase.js"; -import { type ZWaveLogContainer } from "./shared.js"; import { tagify } from "./shared_safe.js"; import { getDirectionPrefix, getNodeTag } from "./shared_safe.js"; +import { type LogContainer } from "./traits.js"; export class ControllerLogger extends ZWaveLoggerBase implements LogNode { - constructor(loggers: ZWaveLogContainer) { + constructor(loggers: LogContainer) { super(loggers, CONTROLLER_LABEL); } diff --git a/packages/core/src/log/ZWaveLoggerBase.ts b/packages/core/src/log/ZWaveLoggerBase.ts index 7f87a412571a..de4aabcf9464 100644 --- a/packages/core/src/log/ZWaveLoggerBase.ts +++ b/packages/core/src/log/ZWaveLoggerBase.ts @@ -2,11 +2,11 @@ import { type LogContext } from "./shared_safe.js"; import type { LogContainer, ZWaveLogger } from "./traits.js"; export class ZWaveLoggerBase { - constructor(loggers: LogContainer, logLabel: string) { + constructor(loggers: LogContainer, logLabel: string) { this.container = loggers; this.logger = this.container.getLogger(logLabel); } public logger: ZWaveLogger; - public container: LogContainer; + public container: LogContainer; } diff --git a/packages/core/src/log/shared.ts b/packages/core/src/log/shared.ts index 25bde369d44c..bf8f7c236089 100644 --- a/packages/core/src/log/shared.ts +++ b/packages/core/src/log/shared.ts @@ -1,313 +1,23 @@ import type { Format, TransformFunction } from "logform"; -import path from "pathe"; -import { MESSAGE, configs } from "triple-beam"; +import { MESSAGE } from "triple-beam"; import winston from "winston"; -import DailyRotateFile from "winston-daily-rotate-file"; -import type Transport from "winston-transport"; -import type { ConsoleTransportInstance } from "winston/lib/winston/transports"; import { colorizer } from "./Colorizer.js"; import { CONTROL_CHAR_WIDTH, LOG_WIDTH, - type LogContext, type ZWaveLogInfo, calculateFirstLineLength, channelPadding, directionPrefixPadding, messageFitsIntoOneLine, messageToLines, - stringToNodeList, timestampFormatShort, timestampPadding, timestampPaddingShort, } from "./shared_safe.js"; -import { type LogContainer } from "./traits.js"; -import { type ZWaveLogger } from "./traits.js"; const { combine, timestamp, label } = winston.format; -const loglevels = configs.npm.levels; -const isTTY = process.stdout.isTTY; -const isUnitTest = process.env.NODE_ENV === "test"; - -export interface LogConfig { - enabled: boolean; - level: string | number; - transports: Transport[]; - logToFile: boolean; - maxFiles: number; - nodeFilter?: number[]; - filename: string; - forceConsole: boolean; -} -/** @internal */ - -export const nonUndefinedLogConfigKeys = [ - "enabled", - "level", - "transports", - "logToFile", - "maxFiles", - "filename", - "forceConsole", -] as const; - -export class ZWaveLogContainer - extends winston.Container - implements LogContainer -{ - private fileTransport: DailyRotateFile | undefined; - private consoleTransport: ConsoleTransportInstance | undefined; - private loglevelVisibleCache = new Map(); - - private logConfig: LogConfig & { level: string } = { - enabled: true, - level: getTransportLoglevel(), - logToFile: !!process.env.LOGTOFILE, - maxFiles: 7, - nodeFilter: stringToNodeList(process.env.LOG_NODES), - transports: undefined as any, - filename: path.join(process.cwd(), `zwavejs_%DATE%.log`), - forceConsole: false, - }; - - constructor(config: Partial = {}) { - super(); - this.updateConfiguration(config); - } - - public getLogger(label: string): ZWaveLogger { - if (!this.has(label)) { - this.add(label, { - transports: this.getAllTransports(), - format: createLoggerFormat(label), - // Accept all logs, no matter what. The individual loggers take care - // of filtering the wrong loglevels - level: "silly", - }); - } - - return this.get(label) as unknown as ZWaveLogger; - } - - public updateConfiguration(config: Partial): void { - // Avoid overwriting configuration settings with undefined if they shouldn't be - for (const key of nonUndefinedLogConfigKeys) { - if (key in config && config[key] === undefined) { - delete config[key]; - } - } - const changedLoggingTarget = (config.logToFile != undefined - && config.logToFile !== this.logConfig.logToFile) - || (config.forceConsole != undefined - && config.forceConsole !== this.logConfig.forceConsole); - - if (typeof config.level === "number") { - config.level = loglevelFromNumber(config.level); - } - const changedLogLevel = config.level != undefined - && config.level !== this.logConfig.level; - - if ( - config.filename != undefined - && !config.filename.includes("%DATE%") - ) { - config.filename += "_%DATE%.log"; - } - const changedFilename = config.filename != undefined - && config.filename !== this.logConfig.filename; - - if (config.maxFiles != undefined) { - if ( - typeof config.maxFiles !== "number" - || config.maxFiles < 1 - || config.maxFiles > 365 - ) { - delete config.maxFiles; - } - } - const changedMaxFiles = config.maxFiles != undefined - && config.maxFiles !== this.logConfig.maxFiles; - - this.logConfig = Object.assign(this.logConfig, config); - - // If the loglevel changed, our cached "is visible" info is out of date - if (changedLogLevel) { - this.loglevelVisibleCache.clear(); - } - - // When the log target (console, file, filename) was changed, recreate the internal transports - // because at least the filename does not update dynamically - // Also do this when configuring the logger for the first time - const recreateInternalTransports = (this.fileTransport == undefined - && this.consoleTransport == undefined) - || changedLoggingTarget - || changedFilename - || changedMaxFiles; - - if (recreateInternalTransports) { - this.fileTransport?.destroy(); - this.fileTransport = undefined; - this.consoleTransport?.destroy(); - this.consoleTransport = undefined; - } - - // When the internal transports or the custom transports were changed, we need to update the loggers - if (recreateInternalTransports || config.transports != undefined) { - this.loggers.forEach((logger) => - logger.configure({ transports: this.getAllTransports() }) - ); - } - } - - public getConfiguration(): LogConfig { - return this.logConfig; - } - - /** Tests whether a log using the given loglevel will be logged */ - public isLoglevelVisible(loglevel: string): boolean { - // If we are not connected to a TTY, not logging to a file and don't have any custom transports, we won't see anything - if ( - !this.fileTransport - && !this.consoleTransport - && (!this.logConfig.transports - || this.logConfig.transports.length === 0) - ) { - return false; - } - - if (!this.loglevelVisibleCache.has(loglevel)) { - this.loglevelVisibleCache.set( - loglevel, - loglevel in loglevels - && loglevels[loglevel] <= loglevels[this.logConfig.level], - ); - } - return this.loglevelVisibleCache.get(loglevel)!; - } - - public destroy(): void { - for (const key in this.loggers) { - this.close(key); - } - - this.fileTransport = undefined; - this.consoleTransport = undefined; - this.logConfig.transports = []; - } - - private getAllTransports(): Transport[] { - return [ - ...this.getInternalTransports(), - ...(this.logConfig.transports ?? []), - ]; - } - - private getInternalTransports(): Transport[] { - const ret: Transport[] = []; - - // If logging is disabled, don't log to any of the default transports - if (!this.logConfig.enabled) { - return ret; - } - - // Log to file only when opted in - if (this.logConfig.logToFile) { - if (!this.fileTransport) { - this.fileTransport = this.createFileTransport(); - } - ret.push(this.fileTransport); - } - - // Console logs can be noise, so only log to console... - if ( - // when in production - !isUnitTest - // and stdout is a TTY while we're not already logging to a file - && ((isTTY && !this.logConfig.logToFile) - // except when the user explicitly wants to - || this.logConfig.forceConsole) - ) { - if (!this.consoleTransport) { - this.consoleTransport = this.createConsoleTransport(); - } - ret.push(this.consoleTransport); - } - - return ret; - } - - private createConsoleTransport(): ConsoleTransportInstance { - return new winston.transports.Console({ - format: createDefaultTransportFormat( - // Only colorize the output if logging to a TTY, otherwise we'll get - // ansi color codes in logfiles or redirected shells - isTTY || isUnitTest, - // Only use short timestamps if logging to a TTY - isTTY, - ), - silent: this.isConsoleTransportSilent(), - }); - } - - private isConsoleTransportSilent(): boolean { - return process.env.NODE_ENV === "test" || !this.logConfig.enabled; - } - - private isFileTransportSilent(): boolean { - return !this.logConfig.enabled; - } - - private createFileTransport(): DailyRotateFile { - const ret = new DailyRotateFile({ - filename: this.logConfig.filename, - auditFile: `${ - this.logConfig.filename - .replace("_%DATE%", "_logrotate") - .replace(/\.log$/, "") - }.json`, - datePattern: "YYYY-MM-DD", - createSymlink: true, - symlinkName: path - .basename(this.logConfig.filename) - .replace(`_%DATE%`, "_current"), - zippedArchive: true, - maxFiles: `${this.logConfig.maxFiles}d`, - format: createDefaultTransportFormat(false, false), - silent: this.isFileTransportSilent(), - }); - ret.on("new", (newFilename: string) => { - console.log(`Logging to file: - ${newFilename}`); - }); - ret.on("error", (err: Error) => { - console.error(`Error in file stream rotator: ${err.message}`); - }); - return ret; - } - - /** - * Checks the log configuration whether logs should be written for a given node id - */ - public isNodeLoggingVisible(nodeId: number): boolean { - // If no filters are set, every node gets logged - if (!this.logConfig.nodeFilter) return true; - return this.logConfig.nodeFilter.includes(nodeId); - } -} - -function getTransportLoglevel(): string { - return process.env.LOGLEVEL! in loglevels ? process.env.LOGLEVEL! : "debug"; -} - -/** Performs a reverse lookup of the numeric loglevel */ -function loglevelFromNumber(numLevel: number | undefined): string | undefined { - if (numLevel == undefined) return; - for (const [level, value] of Object.entries(loglevels)) { - if (value === numLevel) return level; - } -} - /** Creates the common logger format for all loggers under a given channel */ export function createLoggerFormat(channel: string): Format { return combine( diff --git a/packages/core/src/log/shared_safe.ts b/packages/core/src/log/shared_safe.ts index 510f77b5d172..57a999a164ae 100644 --- a/packages/core/src/log/shared_safe.ts +++ b/packages/core/src/log/shared_safe.ts @@ -1,4 +1,6 @@ import type { TransformableInfo } from "logform"; +import type Transport from "winston-transport"; +import { type LogContainer } from "./traits.js"; export const timestampFormatShort = "HH:mm:ss.SSS"; export const timestampPaddingShort = " ".repeat( @@ -143,3 +145,26 @@ export function messageRecordToLines(message: MessageRecord): string[] { .map((line) => line.trimEnd()) ); } +export interface LogConfig { + enabled: boolean; + level: string | number; + transports: Transport[]; + logToFile: boolean; + maxFiles: number; + nodeFilter?: number[]; + filename: string; + forceConsole: boolean; +} + +/** @internal */ +export const nonUndefinedLogConfigKeys = [ + "enabled", + "level", + "transports", + "logToFile", + "maxFiles", + "filename", + "forceConsole", +] as const; + +export type LogFactory = (config?: Partial) => LogContainer; diff --git a/packages/core/src/log/traits.ts b/packages/core/src/log/traits.ts index 1c1a5602a442..e269d7fc8889 100644 --- a/packages/core/src/log/traits.ts +++ b/packages/core/src/log/traits.ts @@ -1,17 +1,26 @@ -import { type LogContext, type ZWaveLogInfo } from "./shared_safe.js"; +import { + type LogConfig, + type LogContext, + type ZWaveLogInfo, +} from "./shared_safe.js"; export interface LogVisibility { isLoglevelVisible(loglevel: string): boolean; isNodeLoggingVisible(nodeId: number): boolean; } -export interface GetLogger { - getLogger(label: string): ZWaveLogger; +export interface GetLogger { + getLogger( + label: string, + ): ZWaveLogger; } export interface ZWaveLogger { log: (info: ZWaveLogInfo) => void; } -export type LogContainer = - & GetLogger - & LogVisibility; + +export interface LogContainer extends GetLogger, LogVisibility { + updateConfiguration(config: Partial): void; + getConfiguration(): LogConfig; + destroy(): void; +} diff --git a/packages/eslint-plugin/src/rules/no-forbidden-imports.ts b/packages/eslint-plugin/src/rules/no-forbidden-imports.ts index 9d3cec86221d..af52099b6764 100644 --- a/packages/eslint-plugin/src/rules/no-forbidden-imports.ts +++ b/packages/eslint-plugin/src/rules/no-forbidden-imports.ts @@ -10,6 +10,7 @@ const whitelistedImports = [ "fflate", "dayjs", "nrf-intel-hex", + "triple-beam", "alcalzone-shared/arrays", "alcalzone-shared/async", "alcalzone-shared/comparable", diff --git a/packages/serial/src/log/Logger.ts b/packages/serial/src/log/Logger.ts index 344db709082e..e8c0954483e8 100644 --- a/packages/serial/src/log/Logger.ts +++ b/packages/serial/src/log/Logger.ts @@ -1,6 +1,6 @@ import { type DataDirection, - type ZWaveLogContainer, + type LogContainer, ZWaveLoggerBase, getDirectionPrefix, } from "@zwave-js/core"; @@ -13,7 +13,7 @@ import { } from "./Logger_safe.js"; export class SerialLogger extends ZWaveLoggerBase { - constructor(loggers: ZWaveLogContainer) { + constructor(loggers: LogContainer) { super(loggers, SERIAL_LABEL); } diff --git a/packages/serial/src/mock/MockPort.ts b/packages/serial/src/mock/MockPort.ts index 17f2756acc7a..faa3b8829600 100644 --- a/packages/serial/src/mock/MockPort.ts +++ b/packages/serial/src/mock/MockPort.ts @@ -1,4 +1,4 @@ -import { ZWaveLogContainer } from "@zwave-js/core"; +import { log as createZWaveLogContainer } from "@zwave-js/core/bindings/log/node"; import type { UnderlyingSink, UnderlyingSource } from "node:stream/web"; import { type ZWaveSerialBindingFactory, @@ -74,7 +74,7 @@ export async function createAndOpenMockedZWaveSerialPort(): Promise<{ const port = new MockPort(); const factory = new ZWaveSerialStreamFactory( port.factory(), - new ZWaveLogContainer({ enabled: false }), + createZWaveLogContainer({ enabled: false }), ); const serial = await factory.createStream(); return { port, serial }; diff --git a/packages/serial/src/serialport/ZWaveSerialStream.ts b/packages/serial/src/serialport/ZWaveSerialStream.ts index 480299fb8570..aa7a00778e04 100644 --- a/packages/serial/src/serialport/ZWaveSerialStream.ts +++ b/packages/serial/src/serialport/ZWaveSerialStream.ts @@ -9,7 +9,7 @@ // 0 --> --> Parsers --> read // 1 └─────────────────┘ └─────────────────┘ └── -import { type ZWaveLogContainer } from "@zwave-js/core"; +import { type LogContainer } from "@zwave-js/core"; import { noop } from "@zwave-js/shared"; import type { ReadableWritablePair, @@ -17,7 +17,6 @@ import type { UnderlyingSource, } from "node:stream/web"; import { SerialLogger } from "../log/Logger.js"; -import { type SerialLogContext } from "../log/Logger_safe.js"; import { MessageHeaders } from "../message/MessageHeaders.js"; import { type ZWaveSerialFrame } from "../parsers/ZWaveSerialFrame.js"; import { ZWaveSerialParser } from "../plumbing/ZWaveSerialParser.js"; @@ -44,7 +43,7 @@ export function isZWaveSerialBindingFactory( export class ZWaveSerialStreamFactory { constructor( binding: ZWaveSerialBindingFactory, - loggers: ZWaveLogContainer, + loggers: LogContainer, ) { this.binding = binding; this.logger = new SerialLogger(loggers); diff --git a/packages/serial/src/zniffer/ZnifferSerialStream.ts b/packages/serial/src/zniffer/ZnifferSerialStream.ts index 93123bc04dc8..ad054b819c37 100644 --- a/packages/serial/src/zniffer/ZnifferSerialStream.ts +++ b/packages/serial/src/zniffer/ZnifferSerialStream.ts @@ -9,7 +9,7 @@ // 0 --> --> Parsers --> read // 1 └─────────────────┘ └─────────────────┘ └── -import { type ZWaveLogContainer } from "@zwave-js/core"; +import { type LogContainer } from "@zwave-js/core"; import { noop } from "@zwave-js/shared"; import type { ReadableWritablePair, @@ -17,7 +17,6 @@ import type { UnderlyingSource, } from "node:stream/web"; import { SerialLogger } from "../log/Logger.js"; -import { type SerialLogContext } from "../log/Logger_safe.js"; import { ZnifferParser } from "../parsers/ZnifferParser.js"; import { type ZnifferSerialFrame } from "../parsers/ZnifferSerialFrame.js"; import { type ZWaveSerialBindingFactory } from "../serialport/ZWaveSerialStream.js"; @@ -26,7 +25,7 @@ import { type ZWaveSerialBindingFactory } from "../serialport/ZWaveSerialStream. export class ZnifferSerialStreamFactory { constructor( binding: ZWaveSerialBindingFactory, - loggers: ZWaveLogContainer, + loggers: LogContainer, ) { this.binding = binding; this.logger = new SerialLogger(loggers); diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index 848ff706913e..e10824335e74 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -61,6 +61,7 @@ import { EncapsulationFlags, type HostIDs, type LogConfig, + type LogContainer, type LogNodeOptions, MAX_SUPERVISION_SESSION_ID, MAX_TRANSPORT_SERVICE_SESSION_ID, @@ -91,7 +92,6 @@ import { type ValueMetadata, ZWaveError, ZWaveErrorCodes, - ZWaveLogContainer, deserializeCacheValue, generateECDHKeyPair, getCCName, @@ -208,7 +208,7 @@ import path from "pathe"; import { PACKAGE_NAME, PACKAGE_VERSION } from "../_version.js"; import { ZWaveController } from "../controller/Controller.js"; import { InclusionState, RemoveNodeReason } from "../controller/Inclusion.js"; -import { type DriverLogContext, DriverLogger } from "../log/Driver.js"; +import { DriverLogger } from "../log/Driver.js"; import type { Endpoint } from "../node/Endpoint.js"; import type { ZWaveNode } from "../node/Node.js"; import { @@ -709,23 +709,9 @@ export class Driver extends TypedEventTarget this.updateUserAgent(this._options.userAgent); } - // Initialize logging - this._logContainer = new ZWaveLogContainer(this._options.logConfig); - this._driverLog = new DriverLogger(this, this._logContainer); - this._controllerLog = new ControllerLogger(this._logContainer); - // Initialize the cache this.cacheDir = this._options.storage.cacheDir; - // Initialize config manager - this.configManager = new ConfigManager({ - logContainer: this._logContainer, - deviceConfigPriorityDir: - this._options.storage.deviceConfigPriorityDir, - deviceConfigExternalDir: - this._options.storage.deviceConfigExternalDir, - }); - const self = this; this.messageEncodingContext = { getHighestSecurityClass: (nodeId) => @@ -916,7 +902,12 @@ export class Driver extends TypedEventTarget return this._networkCache; } - public readonly configManager: ConfigManager; + // This is set during `start()` and should not be accessed before + private _configManager!: ConfigManager; + public get configManager(): ConfigManager { + return this._configManager; + } + public get configVersion(): string { return ( this.configManager?.configVersion @@ -927,14 +918,17 @@ export class Driver extends TypedEventTarget ); } - private _logContainer: ZWaveLogContainer; - private _driverLog: DriverLogger; + // This is set during `start()` and should not be accessed before + private _logContainer!: LogContainer; + // This is set during `start()` and should not be accessed before + private _driverLog!: DriverLogger; /** @internal */ public get driverLog(): DriverLogger { return this._driverLog; } - private _controllerLog: ControllerLogger; + // This is set during `start()` and should not be accessed before + private _controllerLog!: ControllerLogger; /** @internal */ public get controllerLog(): ControllerLogger { return this._controllerLog; @@ -1333,8 +1327,24 @@ export class Driver extends TypedEventTarget ?? (await import("@zwave-js/serial/bindings/node")).serial, db: this._options.host?.db ?? (await import("@zwave-js/core/bindings/db/jsonl")).db, + log: this._options.host?.log + ?? (await import("@zwave-js/core/bindings/log/node")).log, }; + // Initialize logging + this._logContainer = this.bindings.log(this._options.logConfig); + this._driverLog = new DriverLogger(this, this._logContainer); + this._controllerLog = new ControllerLogger(this._logContainer); + + // Initialize config manager + this._configManager = new ConfigManager({ + logContainer: this._logContainer, + deviceConfigPriorityDir: + this._options.storage.deviceConfigPriorityDir, + deviceConfigExternalDir: + this._options.storage.deviceConfigExternalDir, + }); + const spOpenPromise = createDeferredPromise(); // Log which version is running diff --git a/packages/zwave-js/src/lib/driver/ZWaveOptions.ts b/packages/zwave-js/src/lib/driver/ZWaveOptions.ts index 2f145c3c398f..d46e8a8faebe 100644 --- a/packages/zwave-js/src/lib/driver/ZWaveOptions.ts +++ b/packages/zwave-js/src/lib/driver/ZWaveOptions.ts @@ -1,6 +1,7 @@ import type { FileSystem as LegacyFileSystemBindings, LogConfig, + LogFactory, LongRangeChannel, RFRegion, } from "@zwave-js/core"; @@ -142,6 +143,11 @@ export interface ZWaveOptions { * Specifies which bindings are used to interact with the database used to store the cache. */ db?: DatabaseFactory; + + /** + * Specifies the logging implementation to be used + */ + log?: LogFactory; }; storage: { diff --git a/packages/zwave-js/src/lib/log/Driver.ts b/packages/zwave-js/src/lib/log/Driver.ts index e6085471ed9b..ff4140bbdb97 100644 --- a/packages/zwave-js/src/lib/log/Driver.ts +++ b/packages/zwave-js/src/lib/log/Driver.ts @@ -5,9 +5,9 @@ import { } from "@zwave-js/cc"; import { type DataDirection, + type LogContainer, type LogContext, MessagePriority, - type ZWaveLogContainer, ZWaveLoggerBase, getDirectionPrefix, messageRecordToLines, @@ -33,7 +33,7 @@ export interface DriverLogContext extends LogContext<"driver"> { export class DriverLogger extends ZWaveLoggerBase { constructor( private readonly driver: Driver, - loggers: ZWaveLogContainer, + loggers: LogContainer, ) { super(loggers, DRIVER_LABEL); } diff --git a/packages/zwave-js/src/lib/log/Zniffer.ts b/packages/zwave-js/src/lib/log/Zniffer.ts index d5ffea080e41..8d34cac861aa 100644 --- a/packages/zwave-js/src/lib/log/Zniffer.ts +++ b/packages/zwave-js/src/lib/log/Zniffer.ts @@ -5,10 +5,10 @@ import { } from "@zwave-js/cc"; import { type DataDirection, + type LogContainer, type LogContext, type MessageOrCCLogEntry, type RSSI, - type ZWaveLogContainer, ZWaveLoggerBase, getDirectionPrefix, messageRecordToLines, @@ -37,7 +37,7 @@ export interface ZnifferLogContext extends LogContext<"zniffer"> { export class ZnifferLogger extends ZWaveLoggerBase { constructor( private readonly zniffer: Zniffer, - loggers: ZWaveLogContainer, + loggers: LogContainer, ) { super(loggers, ZNIFFER_LABEL); } diff --git a/packages/zwave-js/src/lib/zniffer/Zniffer.ts b/packages/zwave-js/src/lib/zniffer/Zniffer.ts index 2c4efb26756b..9d2cefb1ca75 100644 --- a/packages/zwave-js/src/lib/zniffer/Zniffer.ts +++ b/packages/zwave-js/src/lib/zniffer/Zniffer.ts @@ -12,6 +12,7 @@ import { type FrameType, type HostIDs, type LogConfig, + type LogContainer, MPDUHeaderType, type MaybeNotKnown, NODE_ID_BROADCAST, @@ -25,7 +26,6 @@ import { type UnknownZWaveChipType, ZWaveError, ZWaveErrorCodes, - ZWaveLogContainer, ZnifferRegion, ZnifferRegionLegacy, getChipTypeAndVersion, @@ -76,7 +76,7 @@ import { createDeferredPromise, } from "alcalzone-shared/deferred-promise"; import { type ZWaveOptions } from "../driver/ZWaveOptions.js"; -import { type ZnifferLogContext, ZnifferLogger } from "../log/Zniffer.js"; +import { ZnifferLogger } from "../log/Zniffer.js"; import { type CorruptedFrame, type Frame, @@ -212,10 +212,6 @@ export class Zniffer extends TypedEventTarget { ); } - // Initialize logging - this._logContainer = new ZWaveLogContainer(options.logConfig); - this.znifferLog = new ZnifferLogger(this, this._logContainer); - this._options = options; this._active = false; @@ -314,8 +310,10 @@ export class Zniffer extends TypedEventTarget { return this._supportedFrequencies; } - private _logContainer: ZWaveLogContainer; - private znifferLog: ZnifferLogger; + // This is set during `start()` and should not be accessed before + private _logContainer!: LogContainer; + // This is set during `start()` and should not be accessed before + private znifferLog!: ZnifferLogger; /** The security managers for each node */ private securityManagers: Map { ?? (await import("@zwave-js/core/bindings/fs/node")).fs, serial: this._options.host?.serial ?? (await import("@zwave-js/serial/bindings/node")).serial, + log: this._options.host?.log + ?? (await import("@zwave-js/core/bindings/log/node")).log, }; + // Initialize logging + this._logContainer = this.bindings.log(this._options.logConfig); + this.znifferLog = new ZnifferLogger(this, this._logContainer); + // Open the serial port let binding: ZWaveSerialBindingFactory; if (typeof this.port === "string") {