Skip to content

Commit

Permalink
refactor: pluggable LogContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone committed Jan 31, 2025
1 parent 5c6ac95 commit 08d8ee0
Show file tree
Hide file tree
Showing 20 changed files with 399 additions and 358 deletions.
8 changes: 4 additions & 4 deletions packages/config/src/ConfigManager.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -35,7 +35,7 @@ import {

export interface ConfigManagerOptions {
bindings?: FileSystem;
logContainer?: ZWaveLogContainer<ConfigLogContext>;
logContainer?: LogContainer;
deviceConfigPriorityDir?: string;
deviceConfigExternalDir?: string;
}
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions packages/config/src/Logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
type ZWaveLogContainer,
type LogContainer,
ZWaveLoggerBase,
getDirectionPrefix,
} from "@zwave-js/core";
Expand All @@ -10,7 +10,7 @@ import {
} from "./Logger_safe.js";

export class ConfigLogger extends ZWaveLoggerBase<ConfigLogContext> {
constructor(loggers: ZWaveLogContainer<ConfigLogContext>) {
constructor(loggers: LogContainer) {
super(loggers, CONFIG_LABEL);
}

Expand Down
281 changes: 281 additions & 0 deletions packages/core/src/bindings/log/node.ts
Original file line number Diff line number Diff line change
@@ -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<TContext extends LogContext> extends winston.Container
implements LogContainer
{
private fileTransport: DailyRotateFile | undefined;
private consoleTransport: ConsoleTransportInstance | undefined;
private loglevelVisibleCache = new Map<string, boolean>();

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<LogConfig> = {}) {
super();
this.updateConfiguration(config);
}

public getLogger(label: string): ZWaveLogger<TContext> {
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<TContext>;
}

public updateConfiguration(config: Partial<LogConfig>): 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<LogConfig>) =>
new ZWaveLogContainer(config);
5 changes: 2 additions & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -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";
10 changes: 4 additions & 6 deletions packages/core/src/index_browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/log/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ControllerLogContext>
implements LogNode
{
constructor(loggers: ZWaveLogContainer<ControllerLogContext>) {
constructor(loggers: LogContainer) {
super(loggers, CONTROLLER_LABEL);
}

Expand Down
Loading

0 comments on commit 08d8ee0

Please sign in to comment.