-
-
Notifications
You must be signed in to change notification settings - Fork 634
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: decouple logging from
winston
(#7585)
- Loading branch information
Showing
25 changed files
with
453 additions
and
377 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.