modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
278 lines (242 loc) • 8.19 kB
JavaScript
// logger.js
const LEVELS = ['debug', 'info', 'warn', 'error'];
let currentLevel = 'info';
let enabled = true;
let useColors = true;
let buffering = true;
const COLORS = {
debug: '\x1b[36m', // cyan
info: '\x1b[32m', // green
warn: '\x1b[33m', // yellow
error: '\x1b[31m', // red
reset: '\x1b[0m'
};
let groupLevel = 0;
let globalContext = {};
let buffer = [];
let flushTimeout = null;
const FLUSH_INTERVAL = 300;
const categoryLevels = {};
function getIndent() {
return ' '.repeat(groupLevel);
}
function getTimestamp() {
const d = new Date();
return d.toISOString().replace('T', ' ').replace('Z', '');
}
/**
* Formats a log message according to the specified level and context.
* @param {string} level - Log level (debug, info, warn, error)
* @param {Array<any>} args - Arguments to be logged
* @param {Object} [context={}] - Context object with additional information
* @returns {Array<string>} Formatted log message
*/
function format(level, args, context = {}) {
const timestamp = getTimestamp();
const color = useColors ? COLORS[level] : '';
const reset = useColors ? COLORS.reset : '';
const indent = getIndent();
const transport = globalContext.transport?.toUpperCase?.() || 'UNKNOWN';
let responseTimeStr = '';
if (context.responseTime != null) {
responseTimeStr = ` [responseTime: ${context.responseTime} ms]`;
}
return [`${color}[${timestamp}] [modbus] [${transport}] [${level.toUpperCase()}]${responseTimeStr}`, indent, ...args, reset];
}
/**
* Determines whether a log message should be logged based on the specified level and context.
* @param {string} level - Log level (debug, info, warn, error)
* @param {Object} [context={}] - Context object with additional information
* @returns {boolean} Whether the log message should be logged
*/
function shouldLog(level, context = {}) {
if (!enabled) return false;
if (context.logger && categoryLevels[context.logger]) {
return LEVELS.indexOf(level) >= LEVELS.indexOf(categoryLevels[context.logger]);
}
return LEVELS.indexOf(level) >= LEVELS.indexOf(currentLevel);
}
/**
* Flushes the log buffer and outputs all log messages to the console.
*/
function flushBuffer() {
for (const item of buffer) {
const formatted = format(item.level, item.args, item.context);
if (useColors) {
const [head, indent, ...rest] = formatted;
console[item.level](head + indent, ...rest);
} else {
console[item.level](...formatted);
}
}
buffer = [];
flushTimeout = null;
}
/**
* Outputs a log message to the console.
* @param {string} level - Log level (debug, info, warn, error)
* @param {Array<any>} args - Arguments to be logged
* @param {Object} [context={}] - Context object with additional information
* @param {boolean} [immediate=false] - Whether to output the log message immediately
*/
const LOG_RATE_LIMIT = 100; // ms
let lastLogTime = 0;
async function output(level, args, context, immediate = false) {
if (!shouldLog(level, context)) return;
const now = Date.now();
if (!immediate && now - lastLogTime < LOG_RATE_LIMIT) return;
lastLogTime = now;
if (immediate || !buffering) {
const formatted = format(level, args, context);
if (useColors) {
const [head, indent, ...rest] = formatted;
console[level](head + indent, ...rest);
} else {
console[level](...formatted);
}
return;
}
buffer.push({ level, args, context });
if (!flushTimeout) {
flushTimeout = setTimeout(flushBuffer, FLUSH_INTERVAL);
}
}
/**
* Splits the arguments into the main arguments and the context object.
* @param {Array<any>} args - Arguments to be logged
* @returns {{ args: Array<any>, context: Object }} Object containing the main arguments and the context object
*/
function splitArgsAndContext(args) {
if (args.length > 1 && typeof args[args.length - 1] === 'object' && !Array.isArray(args[args.length - 1])) {
const context = args.pop();
return { args, context };
}
return { args, context: {} };
}
/**
* Logger is a logging utility that supports different log levels and categories.
*
* To log a message, use one of the following methods:
* - `logger.debug()`: Logs a debug message
* - `logger.info()`: Logs an informational message
* - `logger.warn()`: Logs a warning message
* - `logger.error()`: Logs an error message
*
* To create a new logger category, use the `logger.createLogger()` method.
*
* To set the global log level, use the `logger.setLevel()` method.
*
* To set the log level for a specific category, use the `logger.setLevelFor()` method.
*
* To enable or disable logging, use the `logger.enable()` and `logger.disable()` methods.
*
* To get the current log level, use the `logger.getLevel()` method.
*
* To check if logging is enabled, use the `logger.isEnabled()` method.
*
* To disable colored output, use the `logger.disableColors()` method.
*
* To set the global context, use the `logger.setGlobalContext()` method.
*
* To add to the global context, use the `logger.addGlobalContext()` method.
*
* To set the transport type, use the `logger.setTransportType()` method.
*
* To set buffering, use the `logger.setBuffering()` method.
*
* To flush the log buffer, use the `logger.flush()` method.
*/
const logger = {
debug: async (...args) => {
const { args: newArgs, context } = splitArgsAndContext([...args]);
await output('debug', newArgs, context);
},
info: async (...args) => {
const { args: newArgs, context } = splitArgsAndContext([...args]);
await output('info', newArgs, context);
},
warn: async (...args) => {
const { args: newArgs, context } = splitArgsAndContext([...args]);
await output('warn', newArgs, context);
},
error: async (...args) => {
const { args: newArgs, context } = splitArgsAndContext([...args]);
await output('error', newArgs, context, true);
},
group() {
groupLevel++;
},
groupCollapsed() {
groupLevel++;
},
groupEnd() {
if (groupLevel > 0) groupLevel--;
},
setLevel(level) {
if (LEVELS.includes(level)) {
currentLevel = level;
} else {
throw new Error(`Unknown log level: ${level}`);
}
},
setLevelFor(category, level) {
if (!LEVELS.includes(level)) throw new Error(`Unknown log level: ${level}`);
categoryLevels[category] = level;
},
enable() {
enabled = true;
},
disable() {
enabled = false;
},
getLevel() {
return currentLevel;
},
isEnabled() {
return enabled;
},
disableColors() {
useColors = false;
},
setGlobalContext(ctx) {
globalContext = { ...ctx };
},
addGlobalContext(ctx) {
globalContext = { ...globalContext, ...ctx };
},
setTransportType(type) {
globalContext.transport = type;
},
setBuffering(value) {
buffering = !!value;
},
flush() {
flushBuffer();
},
createLogger(name) {
if (!name) throw new Error('Logger name required');
return {
debug: (...args) => {
const { args: newArgs, context } = splitArgsAndContext([...args]);
output('debug', newArgs, { ...context, logger: name });
},
info: (...args) => {
const { args: newArgs, context } = splitArgsAndContext([...args]);
output('info', newArgs, { ...context, logger: name });
},
warn: (...args) => {
const { args: newArgs, context } = splitArgsAndContext([...args]);
output('warn', newArgs, { ...context, logger: name });
},
error: (...args) => {
const { args: newArgs, context } = splitArgsAndContext([...args]);
output('error', newArgs, { ...context, logger: name }, true);
},
group: () => logger.group(),
groupCollapsed: () => logger.groupCollapsed(),
groupEnd: () => logger.groupEnd(),
setLevel: (lvl) => logger.setLevelFor(name, lvl),
};
}
};
module.exports = logger