Skip to content

Commit 2373d7c

Browse files
committed
feat: implement unified JSON/text logging format
- Add logFormat setting to Gateway config (text/json) - Update UI logger to respect global logFormat setting - Add log format selector in Settings UI - Update Debug view to handle both text and JSON log formats - Implement driver logging configuration for JSON mode - Use DailyRotateFile for JSON file transport to maintain symlink functionality - Disable driver internal transports with enabled: false for cleaner config - Add WebSocket streaming for both text and JSON formats - Optimize JSON detection with fast string operations - Add basic tests for logging format functionality This provides a unified logging system where users can switch between human-readable text logs and structured JSON logs across all output destinations: console, files, and WebSocket/UI debug view.
1 parent 3c9348d commit 2373d7c

File tree

9 files changed

+586
-168
lines changed

9 files changed

+586
-168
lines changed

.cursor/commands/lngarrett-lngarrett-requested-review-from-alcalzone-and-robertslando.md

Whitespace-only changes.

.cursor/commands/lngarrett.md

Whitespace-only changes.

api/lib/Gateway.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export type GatewayConfig = {
191191
logEnabled?: boolean
192192
logLevel?: LogLevel
193193
logToFile?: boolean
194+
logFormat?: 'text' | 'json'
194195
values?: GatewayValue[]
195196
jobs?: ScheduledJob[]
196197
plugins?: string[]

api/lib/ZwaveClient.ts

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
} from '@zwave-js/core'
2323
import { createDefaultTransportFormat } from '@zwave-js/core/bindings/log/node'
2424
import { JSONTransport } from '@zwave-js/log-transport-json'
25+
import winston from 'winston'
26+
import DailyRotateFile from 'winston-daily-rotate-file'
2527
import { isDocker } from './utils'
2628
import {
2729
AssociationAddress,
@@ -2318,14 +2320,8 @@ class ZwaveClient extends TypedEventEmitter<ZwaveClientEventCallbacks> {
23182320

23192321
utils.parseSecurityKeys(this.cfg, zwaveOptions)
23202322

2321-
const logTransport = new JSONTransport()
2322-
logTransport.format = createDefaultTransportFormat(true, false)
2323-
2324-
zwaveOptions.logConfig.transports = [logTransport]
2325-
2326-
logTransport.stream.on('data', (data) => {
2327-
this.socket.emit(socketEvents.debug, data.message.toString())
2328-
})
2323+
// Setup driver logging based on format setting
2324+
this.setupDriverLogging(zwaveOptions)
23292325

23302326
try {
23312327
if (shouldUpdateSettings) {
@@ -6946,6 +6942,111 @@ class ZwaveClient extends TypedEventEmitter<ZwaveClientEventCallbacks> {
69466942
}
69476943
}, 1000)
69486944
}
6945+
6946+
/**
6947+
* Setup driver logging based on the configured format (text or JSON)
6948+
* Uses the same logFormat setting as the gateway for consistency
6949+
*/
6950+
private setupDriverLogging(zwaveOptions: PartialZWaveOptions) {
6951+
// Get the log format from gateway settings - this applies to both app and driver logging
6952+
const settings = jsonStore.get(store.settings)
6953+
const logFormat = settings?.gateway?.logFormat || 'text'
6954+
6955+
if (logFormat === 'json') {
6956+
// JSON logging for driver - create custom transports for all outputs
6957+
const transports = []
6958+
6959+
// Custom format to parse driver's JSON strings before Winston formatting
6960+
const parseDriverJsonFormat = winston.format((info) => {
6961+
if (typeof info.message === 'string' && info.message.startsWith('{')) {
6962+
try {
6963+
info.message = JSON.parse(info.message)
6964+
} catch (e) {
6965+
// Keep as string if parsing fails
6966+
}
6967+
}
6968+
return info
6969+
})
6970+
6971+
// Custom console transport for JSON output
6972+
const jsonConsoleTransport = new winston.transports.Console({
6973+
format: winston.format.combine(
6974+
parseDriverJsonFormat(),
6975+
winston.format.timestamp(),
6976+
winston.format.json(),
6977+
),
6978+
})
6979+
transports.push(jsonConsoleTransport)
6980+
6981+
// Custom file transport for JSON output (if file logging is enabled)
6982+
// Use DailyRotateFile to maintain symlink functionality and proper date handling
6983+
if (this.cfg.logToFile) {
6984+
const fileTransport = new DailyRotateFile({
6985+
filename: ZWAVEJS_LOG_FILE,
6986+
auditFile: ZWAVEJS_LOG_FILE.replace(
6987+
'_%DATE%',
6988+
'_logrotate',
6989+
).replace('.log', '.json'),
6990+
datePattern: 'YYYY-MM-DD',
6991+
createSymlink: true,
6992+
symlinkName: 'zwavejs_current.log',
6993+
zippedArchive: true,
6994+
maxFiles: `${this.cfg.maxFiles || 7}d`,
6995+
maxSize: '50m',
6996+
format: winston.format.combine(
6997+
parseDriverJsonFormat(),
6998+
winston.format.timestamp(),
6999+
winston.format.json(),
7000+
),
7001+
})
7002+
transports.push(fileTransport)
7003+
}
7004+
7005+
// Custom WebSocket transport for JSON output
7006+
const jsonTransport = new JSONTransport()
7007+
jsonTransport.format = winston.format.combine(
7008+
parseDriverJsonFormat(),
7009+
winston.format.timestamp(),
7010+
winston.format.json(),
7011+
)
7012+
transports.push(jsonTransport)
7013+
7014+
// Configure driver to use ONLY our custom transports
7015+
// Disable internal transports and use only our custom ones
7016+
zwaveOptions.logConfig = {
7017+
...zwaveOptions.logConfig,
7018+
enabled: false, // This disables ALL internal transports
7019+
raw: true,
7020+
showLogo: false,
7021+
transports: transports, // Use ONLY our custom transports
7022+
}
7023+
7024+
// Stream JSON logs to WebSocket for debug view
7025+
jsonTransport.stream.on('data', (data) => {
7026+
this.socket.emit(socketEvents.debug, data.message.toString())
7027+
})
7028+
} else {
7029+
// Text logging for driver (preserve original behavior)
7030+
// Ensure console output by setting forceConsole: true when logToFile: true
7031+
// This matches the original behavior where console logs were visible
7032+
zwaveOptions.logConfig.forceConsole = true
7033+
7034+
// Add JSONTransport for WebSocket streaming alongside existing transports
7035+
const logTransport = new JSONTransport()
7036+
logTransport.format = createDefaultTransportFormat(true, false)
7037+
7038+
// Add JSONTransport to existing transports instead of replacing them
7039+
if (!zwaveOptions.logConfig.transports) {
7040+
zwaveOptions.logConfig.transports = []
7041+
}
7042+
zwaveOptions.logConfig.transports.push(logTransport)
7043+
7044+
// Stream logs to WebSocket for debug view
7045+
logTransport.stream.on('data', (data) => {
7046+
this.socket.emit(socketEvents.debug, data.message.toString())
7047+
})
7048+
}
7049+
}
69497050
}
69507051

69517052
export default ZwaveClient

api/lib/logger.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,22 @@ export function sanitizedConfig(
7070
/**
7171
* Return a custom logger format
7272
*/
73-
export function customFormat(noColor = false): winston.Logform.Format {
73+
export function customFormat(
74+
noColor = false,
75+
logFormat: 'text' | 'json' = 'text',
76+
): winston.Logform.Format {
7477
noColor = noColor || disableColors
78+
79+
if (logFormat === 'json') {
80+
// JSON format for all outputs
81+
return combine(
82+
timestamp(),
83+
format.errors({ stack: true }),
84+
format.json(),
85+
)
86+
}
87+
88+
// Existing text format
7589
const formats: winston.Logform.Format[] = [
7690
splat(), // used for formats like: logger.log('info', Message %s', strinVal)
7791
timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
@@ -106,7 +120,10 @@ export const logStream = new PassThrough()
106120
/**
107121
* Create the base transports based on settings provided
108122
*/
109-
export function customTransports(config: LoggerConfig): winston.transport[] {
123+
export function customTransports(
124+
config: LoggerConfig,
125+
logFormat: 'text' | 'json' = 'text',
126+
): winston.transport[] {
110127
// setup transports only once (see issue #2937)
111128
if (transportsList) {
112129
return transportsList
@@ -117,15 +134,15 @@ export function customTransports(config: LoggerConfig): winston.transport[] {
117134
if (process.env.ZUI_NO_CONSOLE !== 'true') {
118135
transportsList.push(
119136
new transports.Console({
120-
format: customFormat(),
137+
format: customFormat(false, logFormat),
121138
level: config.level,
122139
stderrLevels: ['error'],
123140
}),
124141
)
125142
}
126143

127144
const streamTransport = new transports.Stream({
128-
format: customFormat(),
145+
format: customFormat(false, logFormat),
129146
level: config.level,
130147
stream: logStream,
131148
})
@@ -137,7 +154,7 @@ export function customTransports(config: LoggerConfig): winston.transport[] {
137154

138155
if (process.env.DISABLE_LOG_ROTATION === 'true') {
139156
fileTransport = new transports.File({
140-
format: customFormat(true),
157+
format: customFormat(true, logFormat),
141158
filename: config.filePath,
142159
level: config.level,
143160
})
@@ -154,7 +171,7 @@ export function customTransports(config: LoggerConfig): winston.transport[] {
154171
maxFiles: process.env.ZUI_LOG_MAXFILES || '7d',
155172
maxSize: process.env.ZUI_LOG_MAXSIZE || '50m',
156173
level: config.level,
157-
format: customFormat(true),
174+
format: customFormat(true, logFormat),
158175
}
159176
fileTransport = new DailyRotateFile(options)
160177

@@ -182,6 +199,7 @@ export function setupLogger(
182199
config?: DeepPartial<GatewayConfig>,
183200
): ModuleLogger {
184201
const sanitized = sanitizedConfig(module, config)
202+
const logFormat = config?.logFormat || 'text'
185203
// Winston automatically reuses an existing module logger
186204
const logger = container.add(module) as ModuleLogger
187205
const moduleName = module.toUpperCase() || '-'
@@ -196,7 +214,7 @@ export function setupLogger(
196214
), // to correctly parse errors
197215
silent: !sanitized.enabled,
198216
level: sanitized.level,
199-
transports: customTransports(sanitized),
217+
transports: customTransports(sanitized, logFormat),
200218
})
201219
logger.module = module
202220
logger.setup = (cfg) => setupLogger(container, module, cfg)

0 commit comments

Comments
 (0)