diff --git a/packages/core/src/bindings/log/browser.ts b/packages/core/src/bindings/log/browser.ts new file mode 100644 index 000000000000..b233c670edd4 --- /dev/null +++ b/packages/core/src/bindings/log/browser.ts @@ -0,0 +1,87 @@ +import colors from "ansi-colors"; +import { MESSAGE } from "triple-beam"; +import { colorizer } from "../../log/Colorizer.js"; +import { + type LogFormat, + combine, + formatLogMessage, + label, + printLogMessage, + timestamp, +} from "../../log/format.js"; +import { + type LogConfig, + type LogContext, + type LogFactory, + type ZWaveLogInfo, + timestampFormatShort, +} from "../../log/shared.js"; +import { type LogContainer, type ZWaveLogger } from "../../log/traits.js"; + +colors.enabled = true; + +function createLoggerFormat( + channel: string, + colorize: boolean, + shortTimestamps: boolean, +): LogFormat { + const formats = [ + label(channel), + shortTimestamps + ? timestamp(timestampFormatShort) + : timestamp(), + formatLogMessage, + colorize ? colorizer(false) : undefined, + printLogMessage(shortTimestamps), + ].filter((f) => f != undefined); + return combine(...formats); +} + +class ConsoleLogContainer implements LogContainer { + #loggers = new Map(); + + updateConfiguration(_config: Partial): void { + // noop + } + getConfiguration(): LogConfig { + return { + enabled: true, + level: "debug", + transports: [], + logToFile: false, + filename: "zwavejs.log", + forceConsole: false, + maxFiles: 0, + }; + } + destroy(): void { + // noop + } + getLogger>( + label: string, + ): ZWaveLogger { + if (!this.#loggers.has(label)) { + const format = createLoggerFormat(label, true, false); + this.#loggers.set(label, { + log: (info: ZWaveLogInfo) => { + info = format.transform(info); + if (info.level === "error") { + console.error(info[MESSAGE]); + } else { + console.log(info[MESSAGE]); + } + }, + }); + } + return this.#loggers.get(label)!; + } + isLoglevelVisible(loglevel: string): boolean { + return loglevel !== "silly"; + } + isNodeLoggingVisible(_nodeId: number): boolean { + return true; + } +} + +export const log: LogFactory = (_config?: Partial) => + new ConsoleLogContainer(); diff --git a/packages/core/src/bindings/log/node.ts b/packages/core/src/bindings/log/node.ts index 2481451f2c74..41c149067fdb 100644 --- a/packages/core/src/bindings/log/node.ts +++ b/packages/core/src/bindings/log/node.ts @@ -1,21 +1,27 @@ import { getenv } from "@zwave-js/shared"; +import { type Format } from "logform"; 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 { colorizer } from "../../log/Colorizer.js"; import { - createDefaultTransportFormat, - createLoggerFormat, -} from "../../log/shared.js"; + combine, + formatLogMessage, + label, + printLogMessage, + timestamp, +} from "../../log/format.js"; import { type LogConfig, type LogContext, type LogFactory, nonUndefinedLogConfigKeys, stringToNodeList, -} from "../../log/shared_safe.js"; + timestampFormatShort, +} from "../../log/shared.js"; import { type LogContainer, type ZWaveLogger } from "../../log/traits.js"; const isTTY = process.stdout.isTTY; @@ -36,9 +42,60 @@ function loglevelFromNumber(numLevel: number | undefined): string | undefined { } } -class ZWaveLogContainer extends winston.Container - implements LogContainer -{ +/** Creates the common logger format for all loggers under a given channel */ +export function createLoggerFormat(channel: string): Format { + return combine( + // add the channel as a label + label(channel), + // default to short timestamps + timestamp(), + ) as unknown as Format; +} + +/** The common logger format for built-in transports */ +export function createDefaultTransportFormat( + colorize: boolean, + shortTimestamps: boolean, +): Format { + const formats = [ + // overwrite the default timestamp format if necessary + shortTimestamps + ? timestamp(timestampFormatShort) + : undefined, + formatLogMessage, + colorize ? colorizer() : undefined, + printLogMessage(shortTimestamps), + ].filter((f) => f != undefined); + return combine(...formats) as unknown as Format; +} + +/** Unsilences the console transport of a logger and returns the original value */ +export function unsilence(logger: winston.Logger): boolean { + const consoleTransport = logger.transports.find( + (t) => (t as any).name === "console", + ); + if (consoleTransport) { + const ret = !!consoleTransport.silent; + consoleTransport.silent = false; + return ret; + } + return false; +} + +/** Restores the console transport of a logger to its original silence state */ +export function restoreSilence( + logger: winston.Logger, + original: boolean, +): void { + const consoleTransport = logger.transports.find( + (t) => (t as any).name === "console", + ); + if (consoleTransport) { + consoleTransport.silent = original; + } +} + +class ZWaveLogContainer extends winston.Container implements LogContainer { private fileTransport: DailyRotateFile | undefined; private consoleTransport: ConsoleTransportInstance | undefined; private loglevelVisibleCache = new Map(); @@ -59,7 +116,9 @@ class ZWaveLogContainer extends winston.Container this.updateConfiguration(config); } - public getLogger(label: string): ZWaveLogger { + public getLogger( + label: string, + ): ZWaveLogger { if (!this.has(label)) { this.add(label, { transports: this.getAllTransports(), diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 97fe958299c5..e05334c9add8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,7 +7,6 @@ export * from "./log/Controller.definitions.js"; export * from "./log/Controller.js"; export * from "./log/ZWaveLoggerBase.js"; export * from "./log/shared.js"; -export * from "./log/shared_safe.js"; export type * from "./log/traits.js"; export * from "./qr/index.js"; export * from "./reflection/decorators.js"; diff --git a/packages/core/src/index_browser.ts b/packages/core/src/index_browser.ts index 7c3e141c390b..3127600498e5 100644 --- a/packages/core/src/index_browser.ts +++ b/packages/core/src/index_browser.ts @@ -8,7 +8,7 @@ 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 * from "./log/shared.js"; export type * from "./log/traits.js"; export * from "./qr/index.js"; export * from "./reflection/decorators.js"; diff --git a/packages/core/src/index_safe.ts b/packages/core/src/index_safe.ts index a5b57059f3c1..8bca53ae8187 100644 --- a/packages/core/src/index_safe.ts +++ b/packages/core/src/index_safe.ts @@ -4,7 +4,7 @@ export * from "./definitions/index.js"; export * from "./dsk/index.js"; export * from "./error/ZWaveError.js"; -export * from "./log/shared_safe.js"; +export * from "./log/shared.js"; // eslint-disable-next-line @zwave-js/no-forbidden-imports -- FIXME: We know this import is safe, but the lint rule doesn't export * from "./qr/index.js"; export * from "./registries/DeviceClasses.js"; diff --git a/packages/core/src/log/Colorizer.ts b/packages/core/src/log/Colorizer.ts index 688fd659c6ba..f16d779ff175 100644 --- a/packages/core/src/log/Colorizer.ts +++ b/packages/core/src/log/Colorizer.ts @@ -1,13 +1,8 @@ import colors from "ansi-colors"; -import { type TransformFunction, format } from "logform"; -import winston from "winston"; -import type { ZWaveLogInfo } from "./shared_safe.js"; -const defaultColors = winston.config.npm.colors; +import { configs } from "triple-beam"; +import { type LogFormat } from "./format.js"; -// This is a placeholder -interface ColorizerOptions { - __foo?: undefined; -} +const defaultColors = configs.npm.colors; const primaryAndInlineTagRegex = /\[([^\]]+)\]/g; @@ -29,44 +24,45 @@ function colorizeTextAndTags( ); } -export const colorizer = format( - (( - info: ZWaveLogInfo, - _opts: ColorizerOptions, - ) => { - const levelColorKey = - defaultColors[info.level as keyof typeof defaultColors] as string; - const textColor = (colors as any)[levelColorKey]; - const bgColor = (colors as any)[getBgColorName(levelColorKey)]; - // Colorize all segments separately - if (typeof info.message === "string") { - info.message = colorizeTextAndTags( - info.message, - textColor, - bgColor, - ); - } else { - info.message = info.message.map((msg) => - colorizeTextAndTags(msg, textColor, bgColor) - ); - } - info.direction = colors.white(info.direction); - if (info.label) { - info.label = colors.gray.inverse(info.label); - } - if (info.timestamp) { - info.timestamp = colors.gray(info.timestamp); - } - if (info.primaryTags) { - info.primaryTags = colorizeTextAndTags( - info.primaryTags, - textColor, - bgColor, - ); - } - if (info.secondaryTags) { - info.secondaryTags = colors.gray(info.secondaryTags); - } - return info; - }) as unknown as TransformFunction, -); +export function colorizer(bg: boolean = true): LogFormat { + return { + transform: (info) => { + const levelColorKey = + defaultColors[info.level as keyof typeof defaultColors]; + const textColor = (colors as any)[levelColorKey]; + const bgColor = bg + ? (colors as any)[getBgColorName(levelColorKey)] + : ((txt: string) => txt); + // Colorize all segments separately + if (typeof info.message === "string") { + info.message = colorizeTextAndTags( + info.message, + textColor, + bgColor, + ); + } else { + info.message = info.message.map((msg) => + colorizeTextAndTags(msg, textColor, bgColor) + ); + } + info.direction = colors.white(info.direction); + if (info.label) { + info.label = colors.gray.inverse(info.label); + } + if (info.timestamp) { + info.timestamp = colors.gray(info.timestamp); + } + if (info.primaryTags) { + info.primaryTags = colorizeTextAndTags( + info.primaryTags, + textColor, + bgColor, + ); + } + if (info.secondaryTags) { + info.secondaryTags = colors.gray(info.secondaryTags); + } + return info; + }, + }; +} diff --git a/packages/core/src/log/Controller.definitions.ts b/packages/core/src/log/Controller.definitions.ts index 9a2cdcafc669..5e1ea08c193e 100644 --- a/packages/core/src/log/Controller.definitions.ts +++ b/packages/core/src/log/Controller.definitions.ts @@ -1,5 +1,5 @@ import { type InterviewStage, type ValueID } from "../index_browser.js"; -import type { DataDirection, LogContext } from "./shared_safe.js"; +import type { DataDirection, LogContext } from "./shared.js"; export const CONTROLLER_LABEL = "CNTRLR"; export const CONTROLLER_LOGLEVEL = "info"; diff --git a/packages/core/src/log/Controller.test.ts b/packages/core/src/log/Controller.test.ts index 3230b98fa348..4f09324aae68 100644 --- a/packages/core/src/log/Controller.test.ts +++ b/packages/core/src/log/Controller.test.ts @@ -1,4 +1,5 @@ import { log as createZWaveLogContainer } from "@zwave-js/core/bindings/log/node"; +import { createDefaultTransportFormat } from "@zwave-js/core/bindings/log/node"; import { beforeEach, test as baseTest } from "vitest"; import { CommandClasses } from "../definitions/CommandClasses.js"; import { InterviewStage } from "../definitions/InterviewStage.js"; @@ -8,7 +9,6 @@ import { assertMessage, } from "../test/SpyTransport.js"; import { ControllerLogger } from "./Controller.js"; -import { createDefaultTransportFormat } from "./shared.js"; // Extend the test conte diff --git a/packages/core/src/log/Controller.ts b/packages/core/src/log/Controller.ts index 5943211fdb9c..0e454d546194 100644 --- a/packages/core/src/log/Controller.ts +++ b/packages/core/src/log/Controller.ts @@ -21,8 +21,8 @@ import { VALUE_LOGLEVEL, } from "./Controller.definitions.js"; import { ZWaveLoggerBase } from "./ZWaveLoggerBase.js"; -import { tagify } from "./shared_safe.js"; -import { getDirectionPrefix, getNodeTag } from "./shared_safe.js"; +import { tagify } from "./shared.js"; +import { getDirectionPrefix, getNodeTag } from "./shared.js"; import { type LogContainer } from "./traits.js"; export class ControllerLogger extends ZWaveLoggerBase diff --git a/packages/core/src/log/ZWaveLoggerBase.ts b/packages/core/src/log/ZWaveLoggerBase.ts index de4aabcf9464..25022a4c5357 100644 --- a/packages/core/src/log/ZWaveLoggerBase.ts +++ b/packages/core/src/log/ZWaveLoggerBase.ts @@ -1,4 +1,4 @@ -import { type LogContext } from "./shared_safe.js"; +import { type LogContext } from "./shared.js"; import type { LogContainer, ZWaveLogger } from "./traits.js"; export class ZWaveLoggerBase { diff --git a/packages/core/src/log/format.ts b/packages/core/src/log/format.ts new file mode 100644 index 000000000000..ff8a751a8959 --- /dev/null +++ b/packages/core/src/log/format.ts @@ -0,0 +1,147 @@ +import { MESSAGE } from "triple-beam"; +import { formatDate } from "../util/date.js"; +import { + CONTROL_CHAR_WIDTH, + LOG_WIDTH, + type ZWaveLogInfo, + calculateFirstLineLength, + channelPadding, + directionPrefixPadding, + messageFitsIntoOneLine, + messageToLines, + timestampPadding, + timestampPaddingShort, +} from "./shared.js"; + +export interface LogFormat { + transform: (info: ZWaveLogInfo) => ZWaveLogInfo; +} + +export function combine(...formats: LogFormat[]): LogFormat { + return { + transform: (info) => { + for (const format of formats) { + info = format.transform(info); + } + return info; + }, + }; +} + +export function label(label: string): LogFormat { + return { + transform: (info) => { + info.label = label; + return info; + }, + }; +} + +export function timestamp(format?: string): LogFormat { + return { + transform: (info) => { + if (format) { + info.timestamp = formatDate(new Date(), format); + } else { + info.timestamp = new Date().toISOString(); + } + return info; + }, + }; +} + +/** Formats the log message and calculates the necessary paddings */ +export const formatLogMessage: LogFormat = { + transform: (info: ZWaveLogInfo) => { + const messageLines = messageToLines(info.message); + const firstMessageLineLength = messageLines[0].length; + info.multiline = messageLines.length > 1 + || !messageFitsIntoOneLine(info, info.message.length); + // Align postfixes to the right + if (info.secondaryTags) { + // Calculate how many spaces are needed to right-align the postfix + // Subtract 1 because the parts are joined by spaces + info.secondaryTagPadding = Math.max( + // -1 has the special meaning that we don't print any padding, + // because the message takes all the available space + -1, + LOG_WIDTH + - 1 + - calculateFirstLineLength(info, firstMessageLineLength), + ); + } + + if (info.multiline) { + // Break long messages into multiple lines + const lines: string[] = []; + let isFirstLine = true; + for (let message of messageLines) { + while (message.length) { + const cut = Math.min( + message.length, + isFirstLine + ? LOG_WIDTH - calculateFirstLineLength(info, 0) - 1 + : LOG_WIDTH - CONTROL_CHAR_WIDTH, + ); + isFirstLine = false; + lines.push(message.slice(0, cut)); + message = message.slice(cut); + } + } + info.message = lines.join("\n"); + } + return info; + }, +}; + +/** Prints a formatted and colorized log message */ +export function printLogMessage(shortTimestamps: boolean): LogFormat { + return { + transform: (info: ZWaveLogInfo) => { + // The formatter has already split the message into multiple lines + const messageLines = messageToLines(info.message); + // Also this can only happen if the user forgot to call the formatter first + if (info.secondaryTagPadding == undefined) { + info.secondaryTagPadding = -1; + } + // Format the first message line + let firstLine = [ + info.primaryTags, + messageLines[0], + info.secondaryTagPadding < 0 + ? undefined + : " ".repeat(info.secondaryTagPadding), + // If the secondary tag padding is zero, the previous segment gets + // filtered out and we have one less space than necessary + info.secondaryTagPadding === 0 && info.secondaryTags + ? " " + info.secondaryTags + : info.secondaryTags, + ] + .filter((item) => !!item) + .join(" "); + // The directional arrows and the optional grouping lines must be prepended + // without adding spaces + firstLine = + `${info.timestamp} ${info.label} ${info.direction}${firstLine}`; + const lines = [firstLine]; + if (info.multiline) { + // Format all message lines but the first + lines.push( + ...messageLines.slice(1).map( + (line) => + // Skip the columns for the timestamp and the channel name + (shortTimestamps + ? timestampPaddingShort + : timestampPadding) + + channelPadding + // Skip the columns for directional arrows + + directionPrefixPadding + + line, + ), + ); + } + info[MESSAGE as any] = lines.join("\n"); + return info; + }, + }; +} diff --git a/packages/core/src/log/shared.ts b/packages/core/src/log/shared.ts index bf8f7c236089..57a999a164ae 100644 --- a/packages/core/src/log/shared.ts +++ b/packages/core/src/log/shared.ts @@ -1,168 +1,170 @@ -import type { Format, TransformFunction } from "logform"; -import { MESSAGE } from "triple-beam"; -import winston from "winston"; -import { colorizer } from "./Colorizer.js"; -import { - CONTROL_CHAR_WIDTH, - LOG_WIDTH, - type ZWaveLogInfo, - calculateFirstLineLength, - channelPadding, - directionPrefixPadding, - messageFitsIntoOneLine, - messageToLines, - timestampFormatShort, - timestampPadding, - timestampPaddingShort, -} from "./shared_safe.js"; - -const { combine, timestamp, label } = winston.format; - -/** Creates the common logger format for all loggers under a given channel */ -export function createLoggerFormat(channel: string): Format { - return combine( - // add the channel as a label - label({ label: channel }), - // default to short timestamps - timestamp(), - ); +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( + timestampFormatShort.length + 1, +); +export const timestampPadding = " ".repeat(new Date().toISOString().length + 1); +/** @internal */ +export const channelPadding = " ".repeat(7); // 6 chars channel name, 1 space + +export type DataDirection = "inbound" | "outbound" | "none"; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function getDirectionPrefix(direction: DataDirection) { + return direction === "inbound" + ? "« " + : direction === "outbound" + ? "» " + : " "; +} +/** The space the directional arrows, grouping brackets and padding occupies */ +export const CONTROL_CHAR_WIDTH = 2; +export const directionPrefixPadding = " ".repeat(CONTROL_CHAR_WIDTH); + +/** + * The width of a log line in (visible) characters, excluding the timestamp and + * label, but including the direction prefix + */ +export const LOG_WIDTH = 80; +/** The width of the columns containing the timestamp and channel */ +export const LOG_PREFIX_WIDTH = 20; + +export interface ZWaveLogInfo + extends Omit +{ + direction: string; + /** Primary tags are printed before the message and must fit into the first line. + * They don't have to be enclosed in square brackets */ + primaryTags?: string; + /** Secondary tags are right-aligned in the first line and printed in a dim color */ + secondaryTags?: string; + secondaryTagPadding?: number; + multiline?: boolean; + timestamp?: string; + label?: string; + message: string | string[]; + context: TContext; +} + +export interface LogContext { + /** Which logger this log came from */ + source: T; + /** An optional identifier to distinguish different log types from the same logger */ + type?: string; +} + +export type MessageRecord = Record; + +export interface MessageOrCCLogEntry { + tags: string[]; + message?: MessageRecord; } -/** Prints a formatted and colorized log message */ -export function createLogMessagePrinter(shortTimestamps: boolean): Format { - return { - transform: ((info: ZWaveLogInfo) => { - // The formatter has already split the message into multiple lines - const messageLines = messageToLines(info.message); - // Also this can only happen if the user forgot to call the formatter first - if (info.secondaryTagPadding == undefined) { - info.secondaryTagPadding = -1; - } - // Format the first message line - let firstLine = [ - info.primaryTags, - messageLines[0], - info.secondaryTagPadding < 0 - ? undefined - : " ".repeat(info.secondaryTagPadding), - // If the secondary tag padding is zero, the previous segment gets - // filtered out and we have one less space than necessary - info.secondaryTagPadding === 0 && info.secondaryTags - ? " " + info.secondaryTags - : info.secondaryTags, - ] - .filter((item) => !!item) - .join(" "); - // The directional arrows and the optional grouping lines must be prepended - // without adding spaces - firstLine = - `${info.timestamp} ${info.label} ${info.direction}${firstLine}`; - const lines = [firstLine]; - if (info.multiline) { - // Format all message lines but the first - lines.push( - ...messageLines.slice(1).map( - (line) => - // Skip the columns for the timestamp and the channel name - (shortTimestamps - ? timestampPaddingShort - : timestampPadding) - + channelPadding - // Skip the columns for directional arrows - + directionPrefixPadding - + line, - ), - ); - } - info[MESSAGE as any] = lines.join("\n"); - return info; - }) as unknown as TransformFunction, - }; +/** Returns the tag used to log node related messages */ +export function getNodeTag(nodeId: number): string { + return "Node " + nodeId.toString().padStart(3, "0"); } -/** Formats the log message and calculates the necessary paddings */ -export const logMessageFormatter: Format = { - transform: ((info: ZWaveLogInfo) => { - const messageLines = messageToLines(info.message); - const firstMessageLineLength = messageLines[0].length; - info.multiline = messageLines.length > 1 - || !messageFitsIntoOneLine(info, info.message.length); - // Align postfixes to the right - if (info.secondaryTags) { - // Calculate how many spaces are needed to right-align the postfix - // Subtract 1 because the parts are joined by spaces - info.secondaryTagPadding = Math.max( - // -1 has the special meaning that we don't print any padding, - // because the message takes all the available space - -1, - LOG_WIDTH - - 1 - - calculateFirstLineLength(info, firstMessageLineLength), - ); - } - - if (info.multiline) { - // Break long messages into multiple lines - const lines: string[] = []; - let isFirstLine = true; - for (let message of messageLines) { - while (message.length) { - const cut = Math.min( - message.length, - isFirstLine - ? LOG_WIDTH - calculateFirstLineLength(info, 0) - 1 - : LOG_WIDTH - CONTROL_CHAR_WIDTH, - ); - isFirstLine = false; - lines.push(message.slice(0, cut)); - message = message.slice(cut); - } - } - info.message = lines.join("\n"); - } - return info; - }) as unknown as TransformFunction, -}; - -/** The common logger format for built-in transports */ -export function createDefaultTransportFormat( - colorize: boolean, - shortTimestamps: boolean, -): Format { - const formats: Format[] = [ - // overwrite the default timestamp format if necessary - shortTimestamps - ? timestamp({ format: timestampFormatShort }) - : undefined, - logMessageFormatter, - colorize ? colorizer() : undefined, - createLogMessagePrinter(shortTimestamps), - ].filter((f): f is Format => !!f); - return combine(...formats); +/** @internal */ +export function stringToNodeList(nodes?: string): number[] | undefined { + if (!nodes) return undefined; + return nodes + .split(",") + .map((n) => parseInt(n)) + .filter((n) => !Number.isNaN(n)); } -/** Unsilences the console transport of a logger and returns the original value */ -export function unsilence(logger: winston.Logger): boolean { - const consoleTransport = logger.transports.find( - (t) => (t as any).name === "console", +/** Wraps an array of strings in square brackets and joins them with spaces */ +export function tagify(tags: string[]): string { + return tags.map((pfx) => `[${pfx}]`).join(" "); +} + +/** + * Calculates the length the first line of a log message would occupy if it is not split + * @param info The message and information to log + * @param firstMessageLineLength The length of the first line of the actual message text, not including pre- and postfixes. + */ +export function calculateFirstLineLength( + info: ZWaveLogInfo, + firstMessageLineLength: number, +): number { + return ( + [ + CONTROL_CHAR_WIDTH - 1, + firstMessageLineLength, + (info.primaryTags || "").length, + (info.secondaryTags || "").length, + ] + // filter out empty parts + .filter((len) => len > 0) + // simulate adding spaces between parts + .reduce((prev, val) => prev + (prev > 0 ? 1 : 0) + val) ); - if (consoleTransport) { - const ret = !!consoleTransport.silent; - consoleTransport.silent = false; - return ret; +} + +/** + * Tests if a given message fits into a single log line + * @param info The message that should be logged + * @param messageLength The length that should be assumed for the actual message without pre and postfixes. + * Can be set to 0 to exclude the message from the calculation + */ +export function messageFitsIntoOneLine( + info: ZWaveLogInfo, + messageLength: number, +): boolean { + const totalLength = calculateFirstLineLength(info, messageLength); + return totalLength <= LOG_WIDTH; +} + +export function messageToLines(message: string | string[]): string[] { + if (typeof message === "string") { + return message.split("\n"); + } else if (message.length > 0) { + return message; + } else { + return [""]; } - return false; } -/** Restores the console transport of a logger to its original silence state */ -export function restoreSilence( - logger: winston.Logger, - original: boolean, -): void { - const consoleTransport = logger.transports.find( - (t) => (t as any).name === "console", +/** Splits a message record into multiple lines and auto-aligns key-value pairs */ +export function messageRecordToLines(message: MessageRecord): string[] { + const entries = Object.entries(message); + if (!entries.length) return []; + + const maxKeyLength = Math.max(...entries.map(([key]) => key.length)); + return entries.flatMap(([key, value]) => + `${key}:${ + " ".repeat( + Math.max(maxKeyLength - key.length + 1, 1), + ) + }${value}` + .split("\n") + .map((line) => line.trimEnd()) ); - if (consoleTransport) { - consoleTransport.silent = original; - } } +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/shared_safe.ts b/packages/core/src/log/shared_safe.ts deleted file mode 100644 index 57a999a164ae..000000000000 --- a/packages/core/src/log/shared_safe.ts +++ /dev/null @@ -1,170 +0,0 @@ -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( - timestampFormatShort.length + 1, -); -export const timestampPadding = " ".repeat(new Date().toISOString().length + 1); -/** @internal */ -export const channelPadding = " ".repeat(7); // 6 chars channel name, 1 space - -export type DataDirection = "inbound" | "outbound" | "none"; - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function getDirectionPrefix(direction: DataDirection) { - return direction === "inbound" - ? "« " - : direction === "outbound" - ? "» " - : " "; -} -/** The space the directional arrows, grouping brackets and padding occupies */ -export const CONTROL_CHAR_WIDTH = 2; -export const directionPrefixPadding = " ".repeat(CONTROL_CHAR_WIDTH); - -/** - * The width of a log line in (visible) characters, excluding the timestamp and - * label, but including the direction prefix - */ -export const LOG_WIDTH = 80; -/** The width of the columns containing the timestamp and channel */ -export const LOG_PREFIX_WIDTH = 20; - -export interface ZWaveLogInfo - extends Omit -{ - direction: string; - /** Primary tags are printed before the message and must fit into the first line. - * They don't have to be enclosed in square brackets */ - primaryTags?: string; - /** Secondary tags are right-aligned in the first line and printed in a dim color */ - secondaryTags?: string; - secondaryTagPadding?: number; - multiline?: boolean; - timestamp?: string; - label?: string; - message: string | string[]; - context: TContext; -} - -export interface LogContext { - /** Which logger this log came from */ - source: T; - /** An optional identifier to distinguish different log types from the same logger */ - type?: string; -} - -export type MessageRecord = Record; - -export interface MessageOrCCLogEntry { - tags: string[]; - message?: MessageRecord; -} - -/** Returns the tag used to log node related messages */ -export function getNodeTag(nodeId: number): string { - return "Node " + nodeId.toString().padStart(3, "0"); -} - -/** @internal */ -export function stringToNodeList(nodes?: string): number[] | undefined { - if (!nodes) return undefined; - return nodes - .split(",") - .map((n) => parseInt(n)) - .filter((n) => !Number.isNaN(n)); -} - -/** Wraps an array of strings in square brackets and joins them with spaces */ -export function tagify(tags: string[]): string { - return tags.map((pfx) => `[${pfx}]`).join(" "); -} - -/** - * Calculates the length the first line of a log message would occupy if it is not split - * @param info The message and information to log - * @param firstMessageLineLength The length of the first line of the actual message text, not including pre- and postfixes. - */ -export function calculateFirstLineLength( - info: ZWaveLogInfo, - firstMessageLineLength: number, -): number { - return ( - [ - CONTROL_CHAR_WIDTH - 1, - firstMessageLineLength, - (info.primaryTags || "").length, - (info.secondaryTags || "").length, - ] - // filter out empty parts - .filter((len) => len > 0) - // simulate adding spaces between parts - .reduce((prev, val) => prev + (prev > 0 ? 1 : 0) + val) - ); -} - -/** - * Tests if a given message fits into a single log line - * @param info The message that should be logged - * @param messageLength The length that should be assumed for the actual message without pre and postfixes. - * Can be set to 0 to exclude the message from the calculation - */ -export function messageFitsIntoOneLine( - info: ZWaveLogInfo, - messageLength: number, -): boolean { - const totalLength = calculateFirstLineLength(info, messageLength); - return totalLength <= LOG_WIDTH; -} - -export function messageToLines(message: string | string[]): string[] { - if (typeof message === "string") { - return message.split("\n"); - } else if (message.length > 0) { - return message; - } else { - return [""]; - } -} - -/** Splits a message record into multiple lines and auto-aligns key-value pairs */ -export function messageRecordToLines(message: MessageRecord): string[] { - const entries = Object.entries(message); - if (!entries.length) return []; - - const maxKeyLength = Math.max(...entries.map(([key]) => key.length)); - return entries.flatMap(([key, value]) => - `${key}:${ - " ".repeat( - Math.max(maxKeyLength - key.length + 1, 1), - ) - }${value}` - .split("\n") - .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 e269d7fc8889..f9f0f884cf61 100644 --- a/packages/core/src/log/traits.ts +++ b/packages/core/src/log/traits.ts @@ -2,7 +2,7 @@ import { type LogConfig, type LogContext, type ZWaveLogInfo, -} from "./shared_safe.js"; +} from "./shared.js"; export interface LogVisibility { isLoglevelVisible(loglevel: string): boolean; diff --git a/packages/core/src/test/SpyTransport.ts b/packages/core/src/test/SpyTransport.ts index 8768bfeb9124..b1f27f5adaea 100644 --- a/packages/core/src/test/SpyTransport.ts +++ b/packages/core/src/test/SpyTransport.ts @@ -3,7 +3,7 @@ import sinon from "sinon"; import { MESSAGE } from "triple-beam"; import { type ExpectStatic } from "vitest"; import Transport from "winston-transport"; -import type { ZWaveLogInfo } from "../log/shared_safe.js"; +import type { ZWaveLogInfo } from "../log/shared.js"; const timestampRegex = /\d{2}\:\d{2}\:\d{2}\.\d{3}/g; const timestampPrefixRegex = new RegExp( diff --git a/packages/web/src/script.ts b/packages/web/src/script.ts index 0886e5ede492..f2403ae2e4c0 100644 --- a/packages/web/src/script.ts +++ b/packages/web/src/script.ts @@ -1,4 +1,4 @@ -import { type LogContainer, type LogFactory } from "@zwave-js/core"; +import { log as createLogContainer } from "@zwave-js/core/bindings/log/browser"; import { Bytes } from "@zwave-js/shared"; import { type Database, @@ -288,43 +288,6 @@ class IndexedDBBackedCache implements Database { } } -const logContainer: LogContainer = { - updateConfiguration: (config) => { - // noop - }, - getConfiguration: () => { - return { - enabled: true, - level: "debug", - transports: [], - logToFile: false, - filename: "zwavejs.log", - forceConsole: false, - maxFiles: 0, - }; - }, - destroy: () => { - // noop - }, - getLogger: (label) => { - return { - log(info) { - if (info.level === "error") { - console.error(info.message); - } else { - console.log(`[${label}]`, info); - } - }, - }; - }, - isLoglevelVisible: (loglevel) => { - return loglevel !== "silly"; - }, - isNodeLoggingVisible: (nodeId) => { - return true; - }, -}; - async function init() { let port: SerialPort; try { @@ -379,13 +342,11 @@ async function init() { }, }; - const logFactory: LogFactory = (config) => logContainer; - const d = new Driver(serialBinding, { host: { fs: webFS, db: dbFactory, - log: logFactory, + log: createLogContainer, serial: { // no listing, no creating by path! }, diff --git a/packages/zwave-js/src/lib/log/Driver.test.ts b/packages/zwave-js/src/lib/log/Driver.test.ts index fce9c982b906..fc50d3fb41ae 100644 --- a/packages/zwave-js/src/lib/log/Driver.test.ts +++ b/packages/zwave-js/src/lib/log/Driver.test.ts @@ -1,9 +1,8 @@ +import { MessagePriority, getDirectionPrefix } from "@zwave-js/core"; import { - MessagePriority, createDefaultTransportFormat, - getDirectionPrefix, -} from "@zwave-js/core"; -import { log as createZWaveLogContainer } from "@zwave-js/core/bindings/log/node"; + log as createZWaveLogContainer, +} from "@zwave-js/core/bindings/log/node"; import { SpyTransport, assertLogInfo, diff --git a/packages/zwave-js/src/lib/test/driver/SerialLogger.test.ts b/packages/zwave-js/src/lib/test/driver/SerialLogger.test.ts index 54e91ede7b0a..174686da0984 100644 --- a/packages/zwave-js/src/lib/test/driver/SerialLogger.test.ts +++ b/packages/zwave-js/src/lib/test/driver/SerialLogger.test.ts @@ -1,5 +1,7 @@ -import { createDefaultTransportFormat } from "@zwave-js/core"; -import { log as createZWaveLogContainer } from "@zwave-js/core/bindings/log/node"; +import { + createDefaultTransportFormat, + log as createZWaveLogContainer, +} from "@zwave-js/core/bindings/log/node"; import { SpyTransport, assertMessage } from "@zwave-js/core/test"; import { SerialLogger } from "@zwave-js/serial"; import { Bytes } from "@zwave-js/shared/safe"; diff --git a/packages/zwave-js/src/lib/test/driver/invalidPayloadLog.test.ts b/packages/zwave-js/src/lib/test/driver/invalidPayloadLog.test.ts index ee0ba9f6d33c..25d8ccf6db7e 100644 --- a/packages/zwave-js/src/lib/test/driver/invalidPayloadLog.test.ts +++ b/packages/zwave-js/src/lib/test/driver/invalidPayloadLog.test.ts @@ -1,4 +1,4 @@ -import { createDefaultTransportFormat } from "@zwave-js/core"; +import { createDefaultTransportFormat } from "@zwave-js/core/bindings/log/node"; import { SpyTransport, assertMessage } from "@zwave-js/core/test"; import { FunctionType } from "@zwave-js/serial"; import { Bytes } from "@zwave-js/shared";