From 2eec6a4d26a69ed279206c7b988d56a9656ecaa2 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sun, 19 May 2024 15:47:40 +0200 Subject: [PATCH] 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 --- lib/extension/bridge.ts | 11 +- lib/extension/homeassistant.ts | 2 +- lib/extension/legacy/bridgeLegacy.ts | 7 +- lib/mqtt.ts | 36 ++++--- lib/types/types.d.ts | 4 +- lib/util/logger.ts | 67 ++++++------ lib/util/settings.schema.json | 19 +++- lib/util/settings.ts | 9 ++ test/bridge.test.js | 20 +++- test/controller.test.js | 16 +-- test/homeassistant.test.js | 2 +- test/logger.test.js | 151 +++++++++++++++++++-------- test/settings.test.js | 10 ++ test/stub/logger.js | 3 + 14 files changed, 244 insertions(+), 113 deletions(-) diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index beed0193b..4615b845f 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -203,6 +203,10 @@ export default class Bridge extends Extension { logger.setLevel(newSettings.advanced.log_level); } + if (newSettings.advanced?.log_namespaced_levels != undefined) { + logger.setNamespacedLevels(newSettings.advanced.log_namespaced_levels); + } + if (newSettings.advanced?.log_debug_namespace_ignore != undefined) { logger.setDebugNamespaceIgnore(newSettings.advanced.log_debug_namespace_ignore); } @@ -358,10 +362,9 @@ export default class Bridge extends Extension { // Deprecated @bind async configLogLevel(message: KeyValue | string): Promise { - const allowed = ['error', 'warn', 'info', 'debug']; - const value = this.getValue(message) as 'error' | 'warn' | 'info' | 'debug'; - if (typeof value !== 'string' || !allowed.includes(value)) { - throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`); + const value = this.getValue(message) as settings.LogLevel; + if (typeof value !== 'string' || !settings.LOG_LEVELS.includes(value)) { + throw new Error(`'${value}' is not an allowed value, allowed: ${settings.LOG_LEVELS}`); } logger.setLevel(value); diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 64a933883..0146b17ca 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -1911,7 +1911,7 @@ export default class HomeAssistant extends Extension { command_topic: `${baseTopic}/request/options`, command_template: '{"options": {"advanced": {"log_level": "{{ value }}" } } }', - options: ['info', 'warn', 'error', 'debug'], + options: settings.LOG_LEVELS, }, }, // Sensors: diff --git a/lib/extension/legacy/bridgeLegacy.ts b/lib/extension/legacy/bridgeLegacy.ts index 4e185771e..9a40eb283 100644 --- a/lib/extension/legacy/bridgeLegacy.ts +++ b/lib/extension/legacy/bridgeLegacy.ts @@ -8,7 +8,6 @@ import bind from 'bind-decorator'; const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`); -const allowedLogLevels = ['error', 'warn', 'info', 'debug']; export default class BridgeLegacy extends Extension { private lastJoinedDeviceName: string = null; @@ -118,12 +117,12 @@ export default class BridgeLegacy extends Extension { } @bind logLevel(topic: string, message: string): void { - const level = message.toLowerCase() as 'error' | 'warn' | 'info' | 'debug'; - if (allowedLogLevels.includes(level)) { + const level = message.toLowerCase() as settings.LogLevel; + if (settings.LOG_LEVELS.includes(level)) { logger.info(`Switching log level to '${level}'`); logger.setLevel(level); } else { - logger.error(`Could not set log level to '${level}'. Allowed level: '${allowedLogLevels.join(',')}'`); + logger.error(`Could not set log level to '${level}'. Allowed level: '${settings.LOG_LEVELS.join(',')}'`); } this.publish(); diff --git a/lib/mqtt.ts b/lib/mqtt.ts index 3ce7970e3..f0c377dd6 100644 --- a/lib/mqtt.ts +++ b/lib/mqtt.ts @@ -6,6 +6,8 @@ import fs from 'fs'; import bind from 'bind-decorator'; import type {QoS} from 'mqtt-packet'; +const NS = 'z2m:mqtt'; + export default class MQTT { private publishedTopics: Set = new Set(); private connectionTimer: NodeJS.Timeout; @@ -22,7 +24,7 @@ export default class MQTT { async connect(): Promise { const mqttSettings = settings.get().mqtt; - logger.info(`Connecting to MQTT server at ${mqttSettings.server}`); + logger.info(`Connecting to MQTT server at ${mqttSettings.server}`, NS); const options: mqtt.IClientOptions = { will: { @@ -38,37 +40,37 @@ export default class MQTT { } if (mqttSettings.keepalive) { - logger.debug(`Using MQTT keepalive: ${mqttSettings.keepalive}`); + logger.debug(`Using MQTT keepalive: ${mqttSettings.keepalive}`, NS); options.keepalive = mqttSettings.keepalive; } if (mqttSettings.ca) { - logger.debug(`MQTT SSL/TLS: Path to CA certificate = ${mqttSettings.ca}`); + logger.debug(`MQTT SSL/TLS: Path to CA certificate = ${mqttSettings.ca}`, NS); options.ca = fs.readFileSync(mqttSettings.ca); } if (mqttSettings.key && mqttSettings.cert) { - logger.debug(`MQTT SSL/TLS: Path to client key = ${mqttSettings.key}`); - logger.debug(`MQTT SSL/TLS: Path to client certificate = ${mqttSettings.cert}`); + logger.debug(`MQTT SSL/TLS: Path to client key = ${mqttSettings.key}`, NS); + logger.debug(`MQTT SSL/TLS: Path to client certificate = ${mqttSettings.cert}`, NS); options.key = fs.readFileSync(mqttSettings.key); options.cert = fs.readFileSync(mqttSettings.cert); } if (mqttSettings.user && mqttSettings.password) { - logger.debug(`Using MQTT login with username: ${mqttSettings.user}`); + logger.debug(`Using MQTT login with username: ${mqttSettings.user}`, NS); options.username = mqttSettings.user; options.password = mqttSettings.password; } else { - logger.debug(`Using MQTT anonymous login`); + logger.debug(`Using MQTT anonymous login`, NS); } if (mqttSettings.client_id) { - logger.debug(`Using MQTT client ID: '${mqttSettings.client_id}'`); + logger.debug(`Using MQTT client ID: '${mqttSettings.client_id}'`, NS); options.clientId = mqttSettings.client_id; } if (mqttSettings.hasOwnProperty('reject_unauthorized') && !mqttSettings.reject_unauthorized) { - logger.debug(`MQTT reject_unauthorized set false, ignoring certificate warnings.`); + logger.debug(`MQTT reject_unauthorized set false, ignoring certificate warnings.`, NS); options.rejectUnauthorized = false; } @@ -85,7 +87,7 @@ export default class MQTT { }); this.client.on('error', (err) => { - logger.error(`MQTT error: ${err.message}`); + logger.error(`MQTT error: ${err.message}`, NS); reject(err); }); this.client.on('message', this.onMessage); @@ -97,11 +99,11 @@ export default class MQTT { clearTimeout(this.connectionTimer); this.connectionTimer = setInterval(() => { if (this.client.reconnecting) { - logger.error('Not connected to MQTT server!'); + logger.error('Not connected to MQTT server!', NS); } }, utils.seconds(10)); - logger.info('Connected to MQTT server'); + logger.info('Connected to MQTT server', NS); await this.publishStateOnline(); if (!this.initialConnect) { @@ -126,7 +128,7 @@ export default class MQTT { await this.publish('bridge/state', utils.availabilityPayload('offline', settings.get()), {retain: true, qos: 0}); this.eventBus.removeListeners(this); - logger.info('Disconnecting from MQTT server'); + logger.info('Disconnecting from MQTT server', NS); this.client?.end(); } @@ -137,7 +139,7 @@ export default class MQTT { @bind public onMessage(topic: string, message: Buffer): void { // Since we subscribe to zigbee2mqtt/# we also receive the message we send ourselves, skip these. if (!this.publishedTopics.has(topic)) { - logger.debug(`Received MQTT message on '${topic}' with data '${message.toString()}'`); + logger.debug(`Received MQTT message on '${topic}' with data '${message.toString()}'`, NS); this.eventBus.emitMQTTMessage({topic, message: message.toString()}); } @@ -175,14 +177,14 @@ export default class MQTT { if (!this.isConnected()) { /* istanbul ignore else */ if (!skipLog) { - logger.error(`Not connected to MQTT server!`); - logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`); + logger.error(`Not connected to MQTT server!`, NS); + logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`, NS); } return; } if (!skipLog) { - logger.debug(`MQTT publish: topic '${topic}', payload '${payload}'`); + logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`, NS); } const actualOptions: mqtt.IClientPublishOptions = {...defaultOptions, ...options}; diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 0e6c158a9..4020d7e10 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -1,4 +1,5 @@ /* eslint-disable camelcase */ +import {LogLevel} from 'lib/util/settings'; import type { Device as ZHDevice, Group as ZHGroup, @@ -191,7 +192,8 @@ declare global { log_output: ('console' | 'file' | 'syslog')[], log_directory: string, log_file: string, - log_level: 'debug' | 'info' | 'error' | 'warn', + log_level: LogLevel, + log_namespaced_levels: Record, log_syslog: KeyValue, log_debug_to_mqtt_frontend: boolean, log_debug_namespace_ignore: string, diff --git a/lib/util/logger.ts b/lib/util/logger.ts index d4b5a8f59..dace3d9c4 100644 --- a/lib/util/logger.ts +++ b/lib/util/logger.ts @@ -7,16 +7,14 @@ import fx from 'mkdir-recursive'; import {rimrafSync} from 'rimraf'; import assert from 'assert'; -const LOG_LEVELS = ['error', 'warning', 'info', 'debug'] as const; -type LogLevel = typeof LOG_LEVELS[number]; - class Logger { - private level: LogLevel; + private level: settings.LogLevel; private output: string[]; private directory: string; private logger: winston.Logger; private fileTransport: winston.transports.FileTransportInstance; private debugNamespaceIgnoreRegex?: RegExp; + private namespacedLevels: Record; public init(): void { // What transports to enable @@ -25,20 +23,18 @@ class Logger { const timestamp = moment(Date.now()).format('YYYY-MM-DD.HH-mm-ss'); this.directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', timestamp); const logFilename = settings.get().advanced.log_file.replace('%TIMESTAMP%', timestamp); - // Determine the log level. - const settingLevel = settings.get().advanced.log_level; - // workaround for syslog<>npm level conflict - this.level = settingLevel === 'warn' ? 'warning' : settingLevel; + this.level = settings.get().advanced.log_level; + this.namespacedLevels = settings.get().advanced.log_namespaced_levels; assert( - LOG_LEVELS.includes(this.level), - `'${this.level}' is not valid log_level, use one of '${LOG_LEVELS.join(', ')}'`, + settings.LOG_LEVELS.includes(this.level), + `'${this.level}' is not valid log_level, use one of '${settings.LOG_LEVELS.join(', ')}'`, ); const timestampFormat = (): string => moment().format(settings.get().advanced.timestamp_format); this.logger = winston.createLogger({ - level: this.level, + level: 'debug', format: winston.format.combine( winston.format.errors({stack: true}), winston.format.timestamp({format: timestampFormat}), @@ -81,9 +77,8 @@ class Logger { // Add file logger when enabled // eslint-disable-next-line max-len // NOTE: the initiation of the logger even when not added as transport tries to create the logging directory - const transportFileOptions: KeyValue = { + const transportFileOptions: winston.transports.FileTransportOptions = { filename: path.join(this.directory, logFilename), - json: false, format: winston.format.printf(/* istanbul ignore next */(info) => { return `[${info.timestamp}] ${info.level}: \t${info.namespace}: ${info.message}`; }), @@ -131,7 +126,6 @@ class Logger { } public addTransport(transport: winston.transport): void { - transport.level = this.level; this.logger.add(transport); } @@ -147,41 +141,48 @@ class Logger { this.debugNamespaceIgnoreRegex = value != '' ? new RegExp(value) : undefined; } - // TODO refactor Z2M level to 'warning' to simplify logic - public getLevel(): LogLevel | 'warn' { - return this.level === 'warning' ? 'warn' : this.level; + public getLevel(): settings.LogLevel { + return this.level; } - public setLevel(level: LogLevel | 'warn'): void { - if (level === 'warn') { - level = 'warning'; - } - + public setLevel(level: settings.LogLevel): void { this.level = level; - this.logger.transports.forEach((transport) => transport.level = this.level); + } + + public getNamespacedLevels(): Record { + return this.namespacedLevels; + } + + public setNamespacedLevels(nsLevels: Record): void { + this.namespacedLevels = nsLevels; + } + + private log(level: settings.LogLevel, message: string, namespace: string): void { + const nsLevel = this.namespacedLevels[namespace] ?? this.level; + + if (settings.LOG_LEVELS.indexOf(level) <= settings.LOG_LEVELS.indexOf(nsLevel)) { + this.logger.log(level, message, {namespace}); + } + } + + public error(message: string, namespace: string = 'z2m'): void { + this.log('error', message, namespace); } public warning(message: string, namespace: string = 'z2m'): void { - this.logger.warning(message, {namespace}); + this.log('warning', message, namespace); } public info(message: string, namespace: string = 'z2m'): void { - this.logger.info(message, {namespace}); + this.log('info', message, namespace); } public debug(message: string, namespace: string = 'z2m'): void { - if (this.level !== 'debug') { - return; - } if (this.debugNamespaceIgnoreRegex?.test(namespace)) { return; } - this.logger.debug(message, {namespace}); - } - - public error(message: string, namespace: string = 'z2m'): void { - this.logger.error(message, {namespace}); + this.log('debug', message, namespace); } // Cleanup any old log directory. diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index a85a3a6da..6559537ab 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -492,11 +492,28 @@ }, "log_level": { "type": "string", - "enum": ["info", "warn", "error", "debug"], + "enum": ["error", "warning", "info", "debug"], "title": "Log level", "description": "Logging level", "default": "info" }, + "log_namespaced_levels": { + "type": "object", + "propertyNames": { + "pattern": "^(z2m|zhc|zh)(:[a-z0-9]{1,})*$" + }, + "additionalProperties": { + "type": "string", + "enum": ["error", "warning", "info", "debug"] + }, + "title": "Log Namespaced Levels", + "description": "Set individual log levels for certain namespaces", + "default": {}, + "examples": [ + {"z2m:mqtt": "warning"}, + {"zh:ember:uart:ash": "info"} + ] + }, "log_debug_to_mqtt_frontend": { "type": "boolean", "title": "Log debug to MQTT and frontend", diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 2c5441e6d..825f55557 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -26,6 +26,10 @@ objectAssignDeep(schema, schemaJson); delete schemaJson.properties.ban; } +/** NOTE: by order of priority, lower index is lower level (more important) */ +export const LOG_LEVELS: readonly string[] = ['error', 'warning', 'info', 'debug'] as const; +export type LogLevel = typeof LOG_LEVELS[number]; + // DEPRECATED ZIGBEE2MQTT_CONFIG: https://github.com/Koenkk/zigbee2mqtt/issues/4697 const file = process.env.ZIGBEE2MQTT_CONFIG ?? data.joinPath('configuration.yaml'); const ajvSetting = new Ajv({allErrors: true}).addKeyword('requiresRestart').compile(schemaJson); @@ -82,6 +86,7 @@ const defaults: RecursivePartial = { log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'), log_file: 'log.log', log_level: /* istanbul ignore next */ process.env.DEBUG ? 'debug' : 'info', + log_namespaced_levels: {}, log_syslog: {}, log_debug_to_mqtt_frontend: false, log_debug_namespace_ignore: '', @@ -186,6 +191,10 @@ function loadSettingsWithDefaults(): void { _settingsWithDefaults.advanced.output = _settings.experimental.output; } + if (_settings.advanced?.log_level === 'warn') { + _settingsWithDefaults.advanced.log_level = 'warning'; + } + // @ts-ignore _settingsWithDefaults.ban && _settingsWithDefaults.blocklist.push(..._settingsWithDefaults.ban); // @ts-ignore diff --git a/test/bridge.test.js b/test/bridge.test.js index 8089e5d33..09f53efa6 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -85,7 +85,7 @@ describe('Bridge', () => { // console.log(MQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/info')[1]) expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/info', - stringify({ "restart_required": false, "commit": version.commitHash, "config": { "advanced": { "legacy_availability_payload": true,"output":"json","adapter_concurrent": null, "adapter_delay": null, "availability_blacklist": [], "availability_blocklist": [], "availability_passlist": [], "availability_whitelist": [], "cache_state": true, "cache_state_persistent": true, "cache_state_send_on_startup": true, "channel": 11, "elapsed": false, "ext_pan_id": [221, 221, 221, 221, 221, 221, 221, 221], "last_seen": "disable", "legacy_api": false, "log_directory": directory, "log_file": "log.log", "log_level": "info", "log_output": ["console", "file"], "log_rotation": true, "log_symlink_current": false, "log_syslog": {}, "log_debug_namespace_ignore": "", "log_debug_to_mqtt_frontend": false, "pan_id": 6754, "report": false, "soft_reset_timeout": 0, "timestamp_format": "YYYY-MM-DD HH:mm:ss" }, "blocklist": [], "device_options": {}, "devices": { "0x000b57fffec6a5b2": { "friendly_name": "bulb", "description": "this is my bulb", "retain": true }, "0x000b57fffec6a5b3": { "friendly_name": "bulb_color", "retain": false }, "0x000b57fffec6a5b4": { "friendly_name": "bulb_color_2", "retain": false }, "0x000b57fffec6a5b7": { "friendly_name": "bulb_2", "retain": false }, "0x0017880104a44559": { "friendly_name": "J1_cover" }, "0x0017880104e43559": { "friendly_name": "U202DST600ZB" }, "0x0017880104e44559": { "friendly_name": "3157100_thermostat" }, "0x0017880104e45517": { "friendly_name": "remote", "retain": true }, "0x0017880104e45520": { "friendly_name": "button", "retain": false }, "0x0017880104e45521": { "friendly_name": "button_double_key", "retain": false }, "0x0017880104e45522": { "friendly_name": "weather_sensor", "qos": 1, "retain": false }, "0x0017880104e45523": { "friendly_name": "occupancy_sensor", "retain": false }, "0x0017880104e45524": { "friendly_name": "power_plug", "retain": false }, "0x0017880104e45526": { "friendly_name": "GL-S-007ZS" }, "0x0017880104e45529": { "friendly_name": "unsupported2", "retain": false }, "0x0017880104e45530": { "friendly_name": "button_double_key_interviewing", "retain": false }, "0x0017880104e45540": { "friendly_name": "ikea_onoff" }, "0x0017880104e45541": { "friendly_name": "wall_switch", "retain": false }, "0x0017880104e45542": { "friendly_name": "wall_switch_double", "retain": false }, "0x0017880104e45543": { "friendly_name": "led_controller_1", "retain": false }, "0x0017880104e45544": { "friendly_name": "led_controller_2", "retain": false }, "0x0017880104e45545": { "friendly_name": "dimmer_wall_switch", "retain": false }, "0x0017880104e45547": { "friendly_name": "curtain", "retain": false }, "0x0017880104e45548": { "friendly_name": "fan", "retain": false }, "0x0017880104e45549": { "friendly_name": "siren", "retain": false }, "0x0017880104e45550": { "friendly_name": "thermostat", "retain": false }, "0x0017880104e45551": { "friendly_name": "smart vent", "retain": false }, "0x0017880104e45552": { "friendly_name": "j1", "retain": false }, "0x0017880104e45553": { "friendly_name": "bulb_enddevice", "retain": false }, "0x0017880104e45559": { "friendly_name": "cc2530_router", "retain": false }, "0x0017880104e45560": { "friendly_name": "livolo", "retain": false }, "0x0017880104e45724":{"friendly_name":"GLEDOPTO_2ID"}, "0xf4ce368a38be56a1":{"cover_1_enabled":"true","cover_1_tilt_enabled":"true","cover_2_enabled":"true","cover_2_tilt_enabled":"true","dimmer_1_dimming_enabled":"true","dimmer_1_enabled":"true","dimmer_2_dimming_enabled":"true","dimmer_2_enabled":"true","dimmer_3_dimming_enabled":"true","dimmer_3_enabled":"true","dimmer_4_dimming_enabled":"true","dimmer_4_enabled":"true","friendly_name":"zigfred_plus","front_surface_enabled":"true","retain":false}, "0x0017882104a44559": { "friendly_name": "TS0601_thermostat" }, "0x0017882104a44560":{"friendly_name":"TS0601_switch"}, "0x0017882104a44562":{"friendly_name":"TS0601_cover_switch"}, "0x0017882194e45543": { "friendly_name": "QS-Zigbee-D02-TRIAC-2C-LN" }, "0x90fd9ffffe4b64aa": { "friendly_name": "SP600_OLD" }, "0x90fd9ffffe4b64ab": { "friendly_name": "SP600_NEW" }, "0x90fd9ffffe4b64ac": { "friendly_name": "MKS-CM-W5" }, "0x90fd9ffffe4b64ae": { "friendly_name": "tradfri_remote", "retain": false }, "0x90fd9ffffe4b64af": { "friendly_name": "roller_shutter" }, "0x90fd9ffffe4b64ax": { "friendly_name": "ZNLDP12LM" }, "0x0017880104e45561": { "friendly_name": "temperature_sensor" }, "0x0017880104e45562": { "friendly_name": "heating_actuator" } }, "external_converters": [], "groups": { "1": { "friendly_name": "group_1", "retain": false }, "9": { "devices": ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], "friendly_name": "ha_discovery_group"}, "11": { "devices": ["bulb_2"], "friendly_name": "group_with_tradfri", "retain": false }, "14": { "devices": ["power_plug", "bulb_2"], "friendly_name": "switch_group", "retain": false }, "12": { "devices": ["TS0601_thermostat"], "friendly_name": "thermostat_group", "retain": false }, "15071": { "devices": ["bulb_color_2", "bulb_2"], "friendly_name": "group_tradfri_remote", "retain": false }, "2": { "friendly_name": "group_2", "retain": false }, "21":{"devices":["GLEDOPTO_2ID/cct"],"friendly_name":"gledopto_group"} }, "homeassistant": false, "map_options": { "graphviz": { "colors": { "fill": { "coordinator": "#e04e5d", "enddevice": "#fff8ce", "router": "#4ea3e0" }, "font": { "coordinator": "#ffffff", "enddevice": "#000000", "router": "#ffffff" }, "line": { "active": "#009900", "inactive": "#994444" } } } }, "mqtt": { "base_topic": "zigbee2mqtt", "include_device_information": false, "server": "mqtt://localhost", "force_disable_retain": false, }, "ota":{"disable_automatic_update_check":false,"update_check_interval":1440}, "passlist": [], "permit_join": true, "serial": { "disable_led": false, "port": "/dev/dummy" } }, "coordinator": { "ieee_address":"0x00124b00120144ae","meta": { "revision": 20190425, "version": 1 }, "type": "z-Stack" }, "log_level": "info", "network": { "channel": 15, "extended_pan_id": [0, 11, 22], "pan_id": 5674 }, "permit_join": false, "version": version.version, "zigbee_herdsman_converters": zhcVersion, "zigbee_herdsman": zhVersion, config_schema: settings.schema }), + stringify({ "restart_required": false, "commit": version.commitHash, "config": { "advanced": { "legacy_availability_payload": true,"output":"json","adapter_concurrent": null, "adapter_delay": null, "availability_blacklist": [], "availability_blocklist": [], "availability_passlist": [], "availability_whitelist": [], "cache_state": true, "cache_state_persistent": true, "cache_state_send_on_startup": true, "channel": 11, "elapsed": false, "ext_pan_id": [221, 221, 221, 221, 221, 221, 221, 221], "last_seen": "disable", "legacy_api": false, "log_directory": directory, "log_file": "log.log", "log_level": "info", "log_namespaced_levels": {}, "log_output": ["console", "file"], "log_rotation": true, "log_symlink_current": false, "log_syslog": {}, "log_debug_namespace_ignore": "", "log_debug_to_mqtt_frontend": false, "pan_id": 6754, "report": false, "soft_reset_timeout": 0, "timestamp_format": "YYYY-MM-DD HH:mm:ss" }, "blocklist": [], "device_options": {}, "devices": { "0x000b57fffec6a5b2": { "friendly_name": "bulb", "description": "this is my bulb", "retain": true }, "0x000b57fffec6a5b3": { "friendly_name": "bulb_color", "retain": false }, "0x000b57fffec6a5b4": { "friendly_name": "bulb_color_2", "retain": false }, "0x000b57fffec6a5b7": { "friendly_name": "bulb_2", "retain": false }, "0x0017880104a44559": { "friendly_name": "J1_cover" }, "0x0017880104e43559": { "friendly_name": "U202DST600ZB" }, "0x0017880104e44559": { "friendly_name": "3157100_thermostat" }, "0x0017880104e45517": { "friendly_name": "remote", "retain": true }, "0x0017880104e45520": { "friendly_name": "button", "retain": false }, "0x0017880104e45521": { "friendly_name": "button_double_key", "retain": false }, "0x0017880104e45522": { "friendly_name": "weather_sensor", "qos": 1, "retain": false }, "0x0017880104e45523": { "friendly_name": "occupancy_sensor", "retain": false }, "0x0017880104e45524": { "friendly_name": "power_plug", "retain": false }, "0x0017880104e45526": { "friendly_name": "GL-S-007ZS" }, "0x0017880104e45529": { "friendly_name": "unsupported2", "retain": false }, "0x0017880104e45530": { "friendly_name": "button_double_key_interviewing", "retain": false }, "0x0017880104e45540": { "friendly_name": "ikea_onoff" }, "0x0017880104e45541": { "friendly_name": "wall_switch", "retain": false }, "0x0017880104e45542": { "friendly_name": "wall_switch_double", "retain": false }, "0x0017880104e45543": { "friendly_name": "led_controller_1", "retain": false }, "0x0017880104e45544": { "friendly_name": "led_controller_2", "retain": false }, "0x0017880104e45545": { "friendly_name": "dimmer_wall_switch", "retain": false }, "0x0017880104e45547": { "friendly_name": "curtain", "retain": false }, "0x0017880104e45548": { "friendly_name": "fan", "retain": false }, "0x0017880104e45549": { "friendly_name": "siren", "retain": false }, "0x0017880104e45550": { "friendly_name": "thermostat", "retain": false }, "0x0017880104e45551": { "friendly_name": "smart vent", "retain": false }, "0x0017880104e45552": { "friendly_name": "j1", "retain": false }, "0x0017880104e45553": { "friendly_name": "bulb_enddevice", "retain": false }, "0x0017880104e45559": { "friendly_name": "cc2530_router", "retain": false }, "0x0017880104e45560": { "friendly_name": "livolo", "retain": false }, "0x0017880104e45724":{"friendly_name":"GLEDOPTO_2ID"}, "0xf4ce368a38be56a1":{"cover_1_enabled":"true","cover_1_tilt_enabled":"true","cover_2_enabled":"true","cover_2_tilt_enabled":"true","dimmer_1_dimming_enabled":"true","dimmer_1_enabled":"true","dimmer_2_dimming_enabled":"true","dimmer_2_enabled":"true","dimmer_3_dimming_enabled":"true","dimmer_3_enabled":"true","dimmer_4_dimming_enabled":"true","dimmer_4_enabled":"true","friendly_name":"zigfred_plus","front_surface_enabled":"true","retain":false}, "0x0017882104a44559": { "friendly_name": "TS0601_thermostat" }, "0x0017882104a44560":{"friendly_name":"TS0601_switch"}, "0x0017882104a44562":{"friendly_name":"TS0601_cover_switch"}, "0x0017882194e45543": { "friendly_name": "QS-Zigbee-D02-TRIAC-2C-LN" }, "0x90fd9ffffe4b64aa": { "friendly_name": "SP600_OLD" }, "0x90fd9ffffe4b64ab": { "friendly_name": "SP600_NEW" }, "0x90fd9ffffe4b64ac": { "friendly_name": "MKS-CM-W5" }, "0x90fd9ffffe4b64ae": { "friendly_name": "tradfri_remote", "retain": false }, "0x90fd9ffffe4b64af": { "friendly_name": "roller_shutter" }, "0x90fd9ffffe4b64ax": { "friendly_name": "ZNLDP12LM" }, "0x0017880104e45561": { "friendly_name": "temperature_sensor" }, "0x0017880104e45562": { "friendly_name": "heating_actuator" } }, "external_converters": [], "groups": { "1": { "friendly_name": "group_1", "retain": false }, "9": { "devices": ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], "friendly_name": "ha_discovery_group"}, "11": { "devices": ["bulb_2"], "friendly_name": "group_with_tradfri", "retain": false }, "14": { "devices": ["power_plug", "bulb_2"], "friendly_name": "switch_group", "retain": false }, "12": { "devices": ["TS0601_thermostat"], "friendly_name": "thermostat_group", "retain": false }, "15071": { "devices": ["bulb_color_2", "bulb_2"], "friendly_name": "group_tradfri_remote", "retain": false }, "2": { "friendly_name": "group_2", "retain": false }, "21":{"devices":["GLEDOPTO_2ID/cct"],"friendly_name":"gledopto_group"} }, "homeassistant": false, "map_options": { "graphviz": { "colors": { "fill": { "coordinator": "#e04e5d", "enddevice": "#fff8ce", "router": "#4ea3e0" }, "font": { "coordinator": "#ffffff", "enddevice": "#000000", "router": "#ffffff" }, "line": { "active": "#009900", "inactive": "#994444" } } } }, "mqtt": { "base_topic": "zigbee2mqtt", "include_device_information": false, "server": "mqtt://localhost", "force_disable_retain": false, }, "ota":{"disable_automatic_update_check":false,"update_check_interval":1440}, "passlist": [], "permit_join": true, "serial": { "disable_led": false, "port": "/dev/dummy" } }, "coordinator": { "ieee_address":"0x00124b00120144ae","meta": { "revision": 20190425, "version": 1 }, "type": "z-Stack" }, "log_level": "info", "network": { "channel": 15, "extended_pan_id": [0, 11, 22], "pan_id": 5674 }, "permit_join": false, "version": version.version, "zigbee_herdsman_converters": zhcVersion, "zigbee_herdsman": zhVersion, config_schema: settings.schema }), { retain: true, qos: 0 }, expect.any(Function) ); @@ -1006,7 +1006,7 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/log_level', - stringify({"data":{},"status":"error","error":"'not_valid' is not an allowed value, allowed: error,warn,info,debug"}), + stringify({"data":{},"status":"error","error":`'not_valid' is not an allowed value, allowed: ${settings.LOG_LEVELS.join(',')}`}), {retain: false, qos: 0}, expect.any(Function) ); }); @@ -1284,7 +1284,7 @@ describe('Bridge', () => { MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_level: 'debug'}}})); await flushPromises(); - expect(logger.getLevel()).toBe('debug'); + expect(logger.getLevel()).toStrictEqual('debug'); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', @@ -1307,6 +1307,20 @@ describe('Bridge', () => { ); }); + it('Change options and apply - log_namespaced_levels', async () => { + logger.setLevel('info'); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: { "z2m:mqtt": 'warning' }}}})); + await flushPromises(); + expect(logger.getNamespacedLevels()).toStrictEqual({ "z2m:mqtt": 'warning' }); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/options', + stringify({"data":{"restart_required":false},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + it('Change options restart required', async () => { zigbeeHerdsman.permitJoin.mockClear(); settings.apply({serial: {port: '123'}}); diff --git a/test/controller.test.js b/test/controller.test.js index 761c549ea..e47c0a510 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -17,6 +17,8 @@ const mocksClear = [ const fs = require('fs'); +const LOG_MQTT_NS = 'z2m:mqtt'; + jest.mock('sd-notify', () => { return { watchdogInterval: () => {return 3000;}, @@ -161,7 +163,7 @@ describe('Controller', () => { logger.error.mockClear(); controller.mqtt.client.reconnecting = true; jest.advanceTimersByTime(11 * 1000); - expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!"); + expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!", LOG_MQTT_NS); controller.mqtt.client.reconnecting = false; }); @@ -174,8 +176,8 @@ describe('Controller', () => { await controller.publishEntityState(device, {state: 'ON', brightness: 50, color_temp: 370, color: {r: 100, g: 50, b: 10}, dummy: {1: 'yes', 2: 'no'}}); await flushPromises(); expect(logger.error).toHaveBeenCalledTimes(2); - expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!"); - expect(logger.error).toHaveBeenCalledWith("Cannot send message: topic: 'zigbee2mqtt/bulb', payload: '{\"brightness\":50,\"color\":{\"b\":10,\"g\":50,\"r\":100},\"color_temp\":370,\"dummy\":{\"1\":\"yes\",\"2\":\"no\"},\"linkquality\":99,\"state\":\"ON\"}"); + expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!", LOG_MQTT_NS); + expect(logger.error).toHaveBeenCalledWith("Cannot send message: topic: 'zigbee2mqtt/bulb', payload: '{\"brightness\":50,\"color\":{\"b\":10,\"g\":50,\"r\":100},\"color_temp\":370,\"dummy\":{\"1\":\"yes\",\"2\":\"no\"},\"linkquality\":99,\"state\":\"ON\"}", LOG_MQTT_NS); controller.mqtt.client.reconnecting = false; }); @@ -215,7 +217,7 @@ describe('Controller', () => { }); await controller.start(); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith('MQTT error: addr not found'); + expect(logger.error).toHaveBeenCalledWith('MQTT error: addr not found', LOG_MQTT_NS); expect(logger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting...'); expect(mockExit).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledWith(1, false); @@ -269,18 +271,18 @@ describe('Controller', () => { await controller.start(); logger.debug.mockClear(); await MQTT.events.message('dummytopic', 'dummymessage'); - expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'dummytopic' with data 'dummymessage'") + expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'dummytopic' with data 'dummymessage'", LOG_MQTT_NS) }); it('Skip MQTT messages on topic we published to', async () => { await controller.start(); logger.debug.mockClear(); await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); - expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'zigbee2mqtt/skip-this-topic' with data 'skipped'") + expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'zigbee2mqtt/skip-this-topic' with data 'skipped'", LOG_MQTT_NS) logger.debug.mockClear(); await controller.mqtt.publish('skip-this-topic', '', {}); await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); - expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug).toHaveBeenCalledTimes(0); }); it('On zigbee event message', async () => { diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index 1defaa241..abd2654a1 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -2363,7 +2363,7 @@ describe('HomeAssistant extension', () => { 'command_topic': 'zigbee2mqtt/bridge/request/options', 'command_template': '{"options": {"advanced": {"log_level": "{{ value }}" } } }', - 'options': ['info', 'warn', 'error', 'debug'], + 'options': settings.LOG_LEVELS, 'origin': origin, 'device': devicePayload, 'availability': [{'topic': 'zigbee2mqtt/bridge/state'}], diff --git a/test/logger.test.js b/test/logger.test.js index d8de17744..25b17c94f 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -4,12 +4,20 @@ let settings; const fs = require('fs'); const path = require('path'); const data = require('./stub/data'); -let stdOutWriteOriginal; const {rimrafSync} = require('rimraf'); const Transport = require('winston-transport'); describe('Logger', () => { let logger; + let consoleWriteSpy; + + beforeAll(() => { + consoleWriteSpy = jest.spyOn(console._stdout, 'write').mockImplementation(() => {}); + }); + + afterAll(() => { + consoleWriteSpy.mockRestore(); + }); beforeEach(async () => { data.writeDefaultConfiguration(); @@ -17,14 +25,12 @@ describe('Logger', () => { settings = require('../lib/util/settings'); settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%'); settings.reRead(); - stdOutWriteOriginal = console._stdout.write; - console._stdout.write = () => {}; logger = require('../lib/util/logger').default; logger.init(); + consoleWriteSpy.mockClear(); }); afterEach(async () => { - console._stdout.write = stdOutWriteOriginal; }); it('Create log directory', () => { @@ -63,20 +69,15 @@ describe('Logger', () => { logger.setLevel('info'); expect(logger.getLevel()).toBe('info'); logger.setLevel('warning'); - expect(logger.getLevel()).toBe('warn');// match old Z2M levels - logger.setLevel('warn'); - expect(logger.getLevel()).toBe('warn');// match old Z2M levels + expect(logger.getLevel()).toBe('warning'); logger.setLevel('error'); expect(logger.getLevel()).toBe('error'); - }); - it('Set warning when log level is warn', () => { - settings.set(['advanced', 'log_level'], 'warn'); - settings.reRead(); - logger.init(); - expect(logger.level).toBe('warning');// getLevel() reports old Z2M level to match display/value - settings.set(['advanced', 'log_level'], 'info'); - settings.reRead(); + // winston level always stays at 'debug', logic handled by custom logger + expect(logger.winston.level).toStrictEqual('debug'); + for (const transport of logger.winston.transports) { + expect(transport.level).toStrictEqual(undefined); + } }); it('Add/remove transport', () => { @@ -145,36 +146,57 @@ describe('Logger', () => { expect(fs.readdirSync(dir.name).includes('current')).toBeTruthy() jest.resetModules(); - logger = require('../lib/util/logger').default; }); - it('Log', () => { - logger.setLevel('debug'); + it.each([ + ['debug', {higher: ['info', 'warning', 'error'], lower: []}], + ['info', {higher: ['warning', 'error'], lower: ['debug']}], + ['warning', {higher: ['error'], lower: ['debug', 'info']}], + ['error', {higher: [], lower: ['debug', 'info', 'warning']}], + ])('Logs relevant levels for %s', (level, otherLevels) => { + logger.setLevel(level); - const debug = jest.spyOn(logger.winston, 'debug'); - logger.debug('debug'); - expect(debug).toHaveBeenCalledWith('debug', {namespace: 'z2m'}); - expect(debug).toHaveBeenCalledTimes(1); + const logSpy = jest.spyOn(logger.winston, 'log'); + consoleWriteSpy.mockClear(); + let i = 1; - const info = jest.spyOn(logger.winston, 'info'); - logger.info('info'); - expect(info).toHaveBeenCalledWith('info', {namespace: 'z2m'}); - expect(info).toHaveBeenCalledTimes(1); + logger[level]('msg'); + expect(logSpy).toHaveBeenLastCalledWith(level, 'msg', {namespace: 'z2m'}); + expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); + logger[level]('msg', 'abcd'); + expect(logSpy).toHaveBeenLastCalledWith(level, 'msg', {namespace: 'abcd'}); + expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); - const warning = jest.spyOn(logger.winston, 'warning'); - logger.warning('warning'); - expect(warning).toHaveBeenCalledWith('warning', {namespace: 'z2m'}); - expect(warning).toHaveBeenCalledTimes(1); + for (const higherLevel of otherLevels.higher) { + logger[higherLevel]('higher msg'); + expect(logSpy).toHaveBeenLastCalledWith(higherLevel, 'higher msg', {namespace: 'z2m'}); + expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); + logger[higherLevel]('higher msg', 'abcd'); + expect(logSpy).toHaveBeenLastCalledWith(higherLevel, 'higher msg', {namespace: 'abcd'}); + expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); + } - const error = jest.spyOn(logger.winston, 'error'); - logger.error('error'); - expect(error).toHaveBeenCalledWith('error', {namespace: 'z2m'}); + logSpy.mockClear(); + consoleWriteSpy.mockClear(); - logger.error(new Error('error'));// test for stack=true - expect(error).toHaveBeenCalledWith('error', {namespace: 'z2m'}); - expect(error).toHaveBeenCalledTimes(2); + for (const lowerLevel of otherLevels.lower) { + logger[lowerLevel]('lower msg'); + expect(logSpy).not.toHaveBeenCalled(); + expect(consoleWriteSpy).not.toHaveBeenCalled(); + logger[lowerLevel]('lower msg', 'abcd'); + expect(logSpy).not.toHaveBeenCalled(); + expect(consoleWriteSpy).not.toHaveBeenCalled(); + } }); + it('Logs Error object', () => { + const logSpy = jest.spyOn(logger.winston, 'log'); + + logger.error(new Error('msg'));// test for stack=true + expect(logSpy).toHaveBeenLastCalledWith('error', new Error('msg'), {namespace: 'z2m'}); + expect(consoleWriteSpy).toHaveBeenCalledTimes(1); + }) + it.each([ [ '^zhc:legacy:fz:(tuya|moes)', @@ -218,7 +240,7 @@ describe('Logger', () => { ], ])('Sets namespace ignore for debug level %s', (ignore, expected, tests) => { logger.setLevel('debug'); - const debugSpy = jest.spyOn(logger.winston, 'debug'); + const logSpy = jest.spyOn(logger.winston, 'log'); logger.setDebugNamespaceIgnore(ignore); expect(logger.debugNamespaceIgnoreRegex).toStrictEqual(expected); expect(logger.getDebugNamespaceIgnore()).toStrictEqual(ignore); @@ -227,12 +249,59 @@ describe('Logger', () => { logger.debug('Test message', test.ns); if (test.match) { - expect(debugSpy).not.toHaveBeenCalled(); + expect(logSpy).not.toHaveBeenCalled(); } else { - expect(debugSpy).toHaveBeenCalled(); + expect(logSpy).toHaveBeenLastCalledWith('debug', 'Test message', {namespace: test.ns}); } - debugSpy.mockClear(); + logSpy.mockClear(); } - }) + }); + + it('Logs with namespaced levels or default - higher', () => { + settings.set(['advanced', 'log_namespaced_levels'], { + 'z2m:mqtt': 'warning', + }); + logger.init(); + logger.setLevel('debug'); + expect(logger.getNamespacedLevels()).toStrictEqual({"z2m:mqtt": 'warning'}); + expect(logger.getLevel()).toStrictEqual('debug'); + + const logSpy = jest.spyOn(logger.winston, 'log'); + + consoleWriteSpy.mockClear(); + logger.info(`MQTT publish: topic 'abcd/efgh', payload '{"my": {"payload": "injson"}}'`, 'z2m:mqtt'); + expect(logSpy).toHaveBeenCalledTimes(0); + expect(consoleWriteSpy).toHaveBeenCalledTimes(0); + logger.error(`Not connected to MQTT server!`, 'z2m:mqtt'); + expect(logSpy).toHaveBeenCalledTimes(1); + expect(consoleWriteSpy).toHaveBeenCalledTimes(1); + logger.info(`Just another info message`, 'z2m:notmqtt'); + expect(logSpy).toHaveBeenCalledTimes(2); + expect(consoleWriteSpy).toHaveBeenCalledTimes(2); + }); + + it('Logs with namespaced levels or default - lower', () => { + expect(logger.getNamespacedLevels()).toStrictEqual({}); + logger.setNamespacedLevels({'z2m:mqtt': 'info'}) + logger.setLevel('warning'); + expect(logger.getNamespacedLevels()).toStrictEqual({"z2m:mqtt": 'info'}); + expect(logger.getLevel()).toStrictEqual('warning'); + + const logSpy = jest.spyOn(logger.winston, 'log'); + + consoleWriteSpy.mockClear(); + logger.info(`MQTT publish: topic 'abcd/efgh', payload '{"my": {"payload": "injson"}}'`, 'z2m:mqtt'); + expect(logSpy).toHaveBeenCalledTimes(1); + expect(consoleWriteSpy).toHaveBeenCalledTimes(1); + logger.error(`Not connected to MQTT server!`, 'z2m:mqtt'); + expect(logSpy).toHaveBeenCalledTimes(2); + expect(consoleWriteSpy).toHaveBeenCalledTimes(2); + logger.info(`Just another info message`, 'z2m:notmqtt'); + expect(logSpy).toHaveBeenCalledTimes(2); + expect(consoleWriteSpy).toHaveBeenCalledTimes(2); + logger.warning(`Just another warning message`, 'z2m:notmqtt'); + expect(logSpy).toHaveBeenCalledTimes(3); + expect(consoleWriteSpy).toHaveBeenCalledTimes(3); + }); }); diff --git a/test/settings.test.js b/test/settings.test.js index 5841a542a..5e5465f17 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -996,4 +996,14 @@ describe('Settings', () => { expect(settings.get().blocklist).toStrictEqual(['blocklist', 'ban']) expect(settings.get().passlist).toStrictEqual(['passlist', 'whitelist']) }); + + it('Deprecated: warn log level', () => { + write(configurationFile, {...minimalConfig, + advanced: {log_level: 'warn'} + }); + + settings.reRead(); + + expect(settings.get().advanced.log_level).toStrictEqual('warning') + }) }); diff --git a/test/stub/logger.js b/test/stub/logger.js index 17d2ac90c..c48be3833 100644 --- a/test/stub/logger.js +++ b/test/stub/logger.js @@ -1,5 +1,6 @@ let level = 'info'; let debugNamespaceIgnore = ''; +let namespacedLevels = {}; let transports = []; @@ -27,6 +28,8 @@ const mock = { }, setLevel: (newLevel) => {level = newLevel}, getLevel: () => level, + setNamespacedLevels: (nsLevels) => {namespacedLevels = nsLevels}, + getNamespacedLevels: () => namespacedLevels, setDebugNamespaceIgnore: (newIgnore) => {debugNamespaceIgnore = newIgnore}, getDebugNamespaceIgnore: () => debugNamespaceIgnore, setTransportsEnabled: (value) => {transportsEnabled = value},