Skip to content

Commit 2eec6a4

Browse files
NerivecKoenkk
andauthored
fix: Add namespace-specific levels support to logger (#22619)
* Add namespaced levels for logger. Add NS to mqtt. Deprecate 'warn'. * Improve setting validation. * Fix setting through frontend * Support reload + frontend improvements * update description * remove requiresRestart * Fix tests. * Fix namespaced logging at lower levels. Add better tests. --------- Co-authored-by: Koen Kanters <[email protected]>
1 parent e9b7a84 commit 2eec6a4

14 files changed

+243
-112
lines changed

lib/extension/bridge.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ export default class Bridge extends Extension {
203203
logger.setLevel(newSettings.advanced.log_level);
204204
}
205205

206+
if (newSettings.advanced?.log_namespaced_levels != undefined) {
207+
logger.setNamespacedLevels(newSettings.advanced.log_namespaced_levels);
208+
}
209+
206210
if (newSettings.advanced?.log_debug_namespace_ignore != undefined) {
207211
logger.setDebugNamespaceIgnore(newSettings.advanced.log_debug_namespace_ignore);
208212
}
@@ -358,10 +362,9 @@ export default class Bridge extends Extension {
358362

359363
// Deprecated
360364
@bind async configLogLevel(message: KeyValue | string): Promise<MQTTResponse> {
361-
const allowed = ['error', 'warn', 'info', 'debug'];
362-
const value = this.getValue(message) as 'error' | 'warn' | 'info' | 'debug';
363-
if (typeof value !== 'string' || !allowed.includes(value)) {
364-
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
365+
const value = this.getValue(message) as settings.LogLevel;
366+
if (typeof value !== 'string' || !settings.LOG_LEVELS.includes(value)) {
367+
throw new Error(`'${value}' is not an allowed value, allowed: ${settings.LOG_LEVELS}`);
365368
}
366369

367370
logger.setLevel(value);

lib/extension/homeassistant.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1911,7 +1911,7 @@ export default class HomeAssistant extends Extension {
19111911
command_topic: `${baseTopic}/request/options`,
19121912
command_template:
19131913
'{"options": {"advanced": {"log_level": "{{ value }}" } } }',
1914-
options: ['info', 'warn', 'error', 'debug'],
1914+
options: settings.LOG_LEVELS,
19151915
},
19161916
},
19171917
// Sensors:

lib/extension/legacy/bridgeLegacy.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import bind from 'bind-decorator';
88

99
const configRegex =
1010
new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`);
11-
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
1211

1312
export default class BridgeLegacy extends Extension {
1413
private lastJoinedDeviceName: string = null;
@@ -118,12 +117,12 @@ export default class BridgeLegacy extends Extension {
118117
}
119118

120119
@bind logLevel(topic: string, message: string): void {
121-
const level = message.toLowerCase() as 'error' | 'warn' | 'info' | 'debug';
122-
if (allowedLogLevels.includes(level)) {
120+
const level = message.toLowerCase() as settings.LogLevel;
121+
if (settings.LOG_LEVELS.includes(level)) {
123122
logger.info(`Switching log level to '${level}'`);
124123
logger.setLevel(level);
125124
} else {
126-
logger.error(`Could not set log level to '${level}'. Allowed level: '${allowedLogLevels.join(',')}'`);
125+
logger.error(`Could not set log level to '${level}'. Allowed level: '${settings.LOG_LEVELS.join(',')}'`);
127126
}
128127

129128
this.publish();

lib/mqtt.ts

+19-17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import fs from 'fs';
66
import bind from 'bind-decorator';
77
import type {QoS} from 'mqtt-packet';
88

9+
const NS = 'z2m:mqtt';
10+
911
export default class MQTT {
1012
private publishedTopics: Set<string> = new Set();
1113
private connectionTimer: NodeJS.Timeout;
@@ -22,7 +24,7 @@ export default class MQTT {
2224

2325
async connect(): Promise<void> {
2426
const mqttSettings = settings.get().mqtt;
25-
logger.info(`Connecting to MQTT server at ${mqttSettings.server}`);
27+
logger.info(`Connecting to MQTT server at ${mqttSettings.server}`, NS);
2628

2729
const options: mqtt.IClientOptions = {
2830
will: {
@@ -38,37 +40,37 @@ export default class MQTT {
3840
}
3941

4042
if (mqttSettings.keepalive) {
41-
logger.debug(`Using MQTT keepalive: ${mqttSettings.keepalive}`);
43+
logger.debug(`Using MQTT keepalive: ${mqttSettings.keepalive}`, NS);
4244
options.keepalive = mqttSettings.keepalive;
4345
}
4446

4547
if (mqttSettings.ca) {
46-
logger.debug(`MQTT SSL/TLS: Path to CA certificate = ${mqttSettings.ca}`);
48+
logger.debug(`MQTT SSL/TLS: Path to CA certificate = ${mqttSettings.ca}`, NS);
4749
options.ca = fs.readFileSync(mqttSettings.ca);
4850
}
4951

5052
if (mqttSettings.key && mqttSettings.cert) {
51-
logger.debug(`MQTT SSL/TLS: Path to client key = ${mqttSettings.key}`);
52-
logger.debug(`MQTT SSL/TLS: Path to client certificate = ${mqttSettings.cert}`);
53+
logger.debug(`MQTT SSL/TLS: Path to client key = ${mqttSettings.key}`, NS);
54+
logger.debug(`MQTT SSL/TLS: Path to client certificate = ${mqttSettings.cert}`, NS);
5355
options.key = fs.readFileSync(mqttSettings.key);
5456
options.cert = fs.readFileSync(mqttSettings.cert);
5557
}
5658

5759
if (mqttSettings.user && mqttSettings.password) {
58-
logger.debug(`Using MQTT login with username: ${mqttSettings.user}`);
60+
logger.debug(`Using MQTT login with username: ${mqttSettings.user}`, NS);
5961
options.username = mqttSettings.user;
6062
options.password = mqttSettings.password;
6163
} else {
62-
logger.debug(`Using MQTT anonymous login`);
64+
logger.debug(`Using MQTT anonymous login`, NS);
6365
}
6466

6567
if (mqttSettings.client_id) {
66-
logger.debug(`Using MQTT client ID: '${mqttSettings.client_id}'`);
68+
logger.debug(`Using MQTT client ID: '${mqttSettings.client_id}'`, NS);
6769
options.clientId = mqttSettings.client_id;
6870
}
6971

7072
if (mqttSettings.hasOwnProperty('reject_unauthorized') && !mqttSettings.reject_unauthorized) {
71-
logger.debug(`MQTT reject_unauthorized set false, ignoring certificate warnings.`);
73+
logger.debug(`MQTT reject_unauthorized set false, ignoring certificate warnings.`, NS);
7274
options.rejectUnauthorized = false;
7375
}
7476

@@ -85,7 +87,7 @@ export default class MQTT {
8587
});
8688

8789
this.client.on('error', (err) => {
88-
logger.error(`MQTT error: ${err.message}`);
90+
logger.error(`MQTT error: ${err.message}`, NS);
8991
reject(err);
9092
});
9193
this.client.on('message', this.onMessage);
@@ -97,11 +99,11 @@ export default class MQTT {
9799
clearTimeout(this.connectionTimer);
98100
this.connectionTimer = setInterval(() => {
99101
if (this.client.reconnecting) {
100-
logger.error('Not connected to MQTT server!');
102+
logger.error('Not connected to MQTT server!', NS);
101103
}
102104
}, utils.seconds(10));
103105

104-
logger.info('Connected to MQTT server');
106+
logger.info('Connected to MQTT server', NS);
105107
await this.publishStateOnline();
106108

107109
if (!this.initialConnect) {
@@ -126,7 +128,7 @@ export default class MQTT {
126128
await this.publish('bridge/state', utils.availabilityPayload('offline', settings.get()),
127129
{retain: true, qos: 0});
128130
this.eventBus.removeListeners(this);
129-
logger.info('Disconnecting from MQTT server');
131+
logger.info('Disconnecting from MQTT server', NS);
130132
this.client?.end();
131133
}
132134

@@ -137,7 +139,7 @@ export default class MQTT {
137139
@bind public onMessage(topic: string, message: Buffer): void {
138140
// Since we subscribe to zigbee2mqtt/# we also receive the message we send ourselves, skip these.
139141
if (!this.publishedTopics.has(topic)) {
140-
logger.debug(`Received MQTT message on '${topic}' with data '${message.toString()}'`);
142+
logger.debug(`Received MQTT message on '${topic}' with data '${message.toString()}'`, NS);
141143
this.eventBus.emitMQTTMessage({topic, message: message.toString()});
142144
}
143145

@@ -175,14 +177,14 @@ export default class MQTT {
175177
if (!this.isConnected()) {
176178
/* istanbul ignore else */
177179
if (!skipLog) {
178-
logger.error(`Not connected to MQTT server!`);
179-
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`);
180+
logger.error(`Not connected to MQTT server!`, NS);
181+
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`, NS);
180182
}
181183
return;
182184
}
183185

184186
if (!skipLog) {
185-
logger.debug(`MQTT publish: topic '${topic}', payload '${payload}'`);
187+
logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`, NS);
186188
}
187189

188190
const actualOptions: mqtt.IClientPublishOptions = {...defaultOptions, ...options};

lib/types/types.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable camelcase */
2+
import {LogLevel} from 'lib/util/settings';
23
import type {
34
Device as ZHDevice,
45
Group as ZHGroup,
@@ -191,7 +192,8 @@ declare global {
191192
log_output: ('console' | 'file' | 'syslog')[],
192193
log_directory: string,
193194
log_file: string,
194-
log_level: 'debug' | 'info' | 'error' | 'warn',
195+
log_level: LogLevel,
196+
log_namespaced_levels: Record<string, LogLevel>,
195197
log_syslog: KeyValue,
196198
log_debug_to_mqtt_frontend: boolean,
197199
log_debug_namespace_ignore: string,

lib/util/logger.ts

+33-32
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,14 @@ import fx from 'mkdir-recursive';
77
import {rimrafSync} from 'rimraf';
88
import assert from 'assert';
99

10-
const LOG_LEVELS = ['error', 'warning', 'info', 'debug'] as const;
11-
type LogLevel = typeof LOG_LEVELS[number];
12-
1310
class Logger {
14-
private level: LogLevel;
11+
private level: settings.LogLevel;
1512
private output: string[];
1613
private directory: string;
1714
private logger: winston.Logger;
1815
private fileTransport: winston.transports.FileTransportInstance;
1916
private debugNamespaceIgnoreRegex?: RegExp;
17+
private namespacedLevels: Record<string, settings.LogLevel>;
2018

2119
public init(): void {
2220
// What transports to enable
@@ -25,20 +23,18 @@ class Logger {
2523
const timestamp = moment(Date.now()).format('YYYY-MM-DD.HH-mm-ss');
2624
this.directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', timestamp);
2725
const logFilename = settings.get().advanced.log_file.replace('%TIMESTAMP%', timestamp);
28-
// Determine the log level.
29-
const settingLevel = settings.get().advanced.log_level;
30-
// workaround for syslog<>npm level conflict
31-
this.level = settingLevel === 'warn' ? 'warning' : settingLevel;
26+
this.level = settings.get().advanced.log_level;
27+
this.namespacedLevels = settings.get().advanced.log_namespaced_levels;
3228

3329
assert(
34-
LOG_LEVELS.includes(this.level),
35-
`'${this.level}' is not valid log_level, use one of '${LOG_LEVELS.join(', ')}'`,
30+
settings.LOG_LEVELS.includes(this.level),
31+
`'${this.level}' is not valid log_level, use one of '${settings.LOG_LEVELS.join(', ')}'`,
3632
);
3733

3834
const timestampFormat = (): string => moment().format(settings.get().advanced.timestamp_format);
3935

4036
this.logger = winston.createLogger({
41-
level: this.level,
37+
level: 'debug',
4238
format: winston.format.combine(
4339
winston.format.errors({stack: true}),
4440
winston.format.timestamp({format: timestampFormat}),
@@ -81,9 +77,8 @@ class Logger {
8177
// Add file logger when enabled
8278
// eslint-disable-next-line max-len
8379
// NOTE: the initiation of the logger even when not added as transport tries to create the logging directory
84-
const transportFileOptions: KeyValue = {
80+
const transportFileOptions: winston.transports.FileTransportOptions = {
8581
filename: path.join(this.directory, logFilename),
86-
json: false,
8782
format: winston.format.printf(/* istanbul ignore next */(info) => {
8883
return `[${info.timestamp}] ${info.level}: \t${info.namespace}: ${info.message}`;
8984
}),
@@ -131,7 +126,6 @@ class Logger {
131126
}
132127

133128
public addTransport(transport: winston.transport): void {
134-
transport.level = this.level;
135129
this.logger.add(transport);
136130
}
137131

@@ -147,41 +141,48 @@ class Logger {
147141
this.debugNamespaceIgnoreRegex = value != '' ? new RegExp(value) : undefined;
148142
}
149143

150-
// TODO refactor Z2M level to 'warning' to simplify logic
151-
public getLevel(): LogLevel | 'warn' {
152-
return this.level === 'warning' ? 'warn' : this.level;
144+
public getLevel(): settings.LogLevel {
145+
return this.level;
153146
}
154147

155-
public setLevel(level: LogLevel | 'warn'): void {
156-
if (level === 'warn') {
157-
level = 'warning';
148+
public setLevel(level: settings.LogLevel): void {
149+
this.level = level;
150+
}
151+
152+
public getNamespacedLevels(): Record<string, settings.LogLevel> {
153+
return this.namespacedLevels;
154+
}
155+
156+
public setNamespacedLevels(nsLevels: Record<string, settings.LogLevel>): void {
157+
this.namespacedLevels = nsLevels;
158+
}
159+
160+
private log(level: settings.LogLevel, message: string, namespace: string): void {
161+
const nsLevel = this.namespacedLevels[namespace] ?? this.level;
162+
163+
if (settings.LOG_LEVELS.indexOf(level) <= settings.LOG_LEVELS.indexOf(nsLevel)) {
164+
this.logger.log(level, message, {namespace});
158165
}
166+
}
159167

160-
this.level = level;
161-
this.logger.transports.forEach((transport) => transport.level = this.level);
168+
public error(message: string, namespace: string = 'z2m'): void {
169+
this.log('error', message, namespace);
162170
}
163171

164172
public warning(message: string, namespace: string = 'z2m'): void {
165-
this.logger.warning(message, {namespace});
173+
this.log('warning', message, namespace);
166174
}
167175

168176
public info(message: string, namespace: string = 'z2m'): void {
169-
this.logger.info(message, {namespace});
177+
this.log('info', message, namespace);
170178
}
171179

172180
public debug(message: string, namespace: string = 'z2m'): void {
173-
if (this.level !== 'debug') {
174-
return;
175-
}
176181
if (this.debugNamespaceIgnoreRegex?.test(namespace)) {
177182
return;
178183
}
179184

180-
this.logger.debug(message, {namespace});
181-
}
182-
183-
public error(message: string, namespace: string = 'z2m'): void {
184-
this.logger.error(message, {namespace});
185+
this.log('debug', message, namespace);
185186
}
186187

187188
// Cleanup any old log directory.

lib/util/settings.schema.json

+18-1
Original file line numberDiff line numberDiff line change
@@ -492,11 +492,28 @@
492492
},
493493
"log_level": {
494494
"type": "string",
495-
"enum": ["info", "warn", "error", "debug"],
495+
"enum": ["error", "warning", "info", "debug"],
496496
"title": "Log level",
497497
"description": "Logging level",
498498
"default": "info"
499499
},
500+
"log_namespaced_levels": {
501+
"type": "object",
502+
"propertyNames": {
503+
"pattern": "^(z2m|zhc|zh)(:[a-z0-9]{1,})*$"
504+
},
505+
"additionalProperties": {
506+
"type": "string",
507+
"enum": ["error", "warning", "info", "debug"]
508+
},
509+
"title": "Log Namespaced Levels",
510+
"description": "Set individual log levels for certain namespaces",
511+
"default": {},
512+
"examples": [
513+
{"z2m:mqtt": "warning"},
514+
{"zh:ember:uart:ash": "info"}
515+
]
516+
},
500517
"log_debug_to_mqtt_frontend": {
501518
"type": "boolean",
502519
"title": "Log debug to MQTT and frontend",

lib/util/settings.ts

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ objectAssignDeep(schema, schemaJson);
2626
delete schemaJson.properties.ban;
2727
}
2828

29+
/** NOTE: by order of priority, lower index is lower level (more important) */
30+
export const LOG_LEVELS: readonly string[] = ['error', 'warning', 'info', 'debug'] as const;
31+
export type LogLevel = typeof LOG_LEVELS[number];
32+
2933
// DEPRECATED ZIGBEE2MQTT_CONFIG: https://github.com/Koenkk/zigbee2mqtt/issues/4697
3034
const file = process.env.ZIGBEE2MQTT_CONFIG ?? data.joinPath('configuration.yaml');
3135
const ajvSetting = new Ajv({allErrors: true}).addKeyword('requiresRestart').compile(schemaJson);
@@ -82,6 +86,7 @@ const defaults: RecursivePartial<Settings> = {
8286
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
8387
log_file: 'log.log',
8488
log_level: /* istanbul ignore next */ process.env.DEBUG ? 'debug' : 'info',
89+
log_namespaced_levels: {},
8590
log_syslog: {},
8691
log_debug_to_mqtt_frontend: false,
8792
log_debug_namespace_ignore: '',
@@ -186,6 +191,10 @@ function loadSettingsWithDefaults(): void {
186191
_settingsWithDefaults.advanced.output = _settings.experimental.output;
187192
}
188193

194+
if (_settings.advanced?.log_level === 'warn') {
195+
_settingsWithDefaults.advanced.log_level = 'warning';
196+
}
197+
189198
// @ts-ignore
190199
_settingsWithDefaults.ban && _settingsWithDefaults.blocklist.push(..._settingsWithDefaults.ban);
191200
// @ts-ignore

0 commit comments

Comments
 (0)