dynamixel
Version:
Node.js library for controlling DYNAMIXEL servo motors via U2D2 interface with Protocol 2.0 support
430 lines (364 loc) • 10.9 kB
JavaScript
import { EventEmitter } from 'events';
import { performance } from 'perf_hooks';
/**
* Enhanced Logger for DYNAMIXEL library
* Inspired by DynaNode's Logger architecture
* Provides structured logging with performance metrics and filtering
*/
export class Logger extends EventEmitter {
constructor(options = {}) {
super();
this.level = options.level || 'info';
this.enableConsole = options.enableConsole !== false;
this.enableFile = options.enableFile || false;
this.maxLogEntries = options.maxLogEntries || 1000;
this.enablePerformanceMetrics = options.enablePerformanceMetrics || false;
this.logLevels = {
trace: 0,
debug: 1,
info: 2,
warn: 3,
error: 4,
fatal: 5
};
this.logs = [];
this.performanceMetrics = new Map();
this.logCount = { trace: 0, debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
this.setupFormatters();
}
setupFormatters() {
this.formatters = {
console: (entry) => {
const timestamp = new Date(entry.timestamp).toISOString();
const level = entry.level.toUpperCase().padEnd(5);
const device = entry.deviceId ? `[ID:${entry.deviceId}]` : '';
const category = entry.category ? `[${entry.category}]` : '';
return `${timestamp} ${level} ${device}${category} ${entry.message}`;
},
json: (entry) => JSON.stringify(entry),
structured: (entry) => ({
timestamp: entry.timestamp,
level: entry.level,
message: entry.message,
deviceId: entry.deviceId,
category: entry.category,
data: entry.data,
performance: entry.performance
})
};
}
/**
* Check if a log level should be output
*/
shouldLog(level) {
return this.logLevels[level] >= this.logLevels[this.level];
}
/**
* Core logging method
*/
log(level, message, data = {}) {
if (!this.shouldLog(level)) return;
const entry = {
timestamp: Date.now(),
level,
message,
deviceId: data.deviceId,
category: data.category || 'general',
data: { ...data },
performance: data.performance
};
// Remove meta fields from data
delete entry.data.deviceId;
delete entry.data.category;
delete entry.data.performance;
this.logCount[level]++;
this.logs.push(entry);
// Maintain log size limit
if (this.logs.length > this.maxLogEntries) {
this.logs.shift();
}
// Output to console
if (this.enableConsole) {
this.outputToConsole(entry);
}
// Emit log event
this.emit('log', entry);
this.emit(`log:${level}`, entry);
if (entry.category) {
this.emit(`log:${entry.category}`, entry);
}
}
outputToConsole(entry) {
const formatted = this.formatters.console(entry);
switch (entry.level) {
case 'trace':
case 'debug':
console.debug(formatted);
break;
case 'info':
console.info(formatted);
break;
case 'warn':
console.warn(formatted);
break;
case 'error':
case 'fatal':
console.error(formatted);
break;
}
// Log additional data if present
if (Object.keys(entry.data).length > 0) {
console.log(' Data:', entry.data);
}
if (entry.performance) {
console.log(' Performance:', entry.performance);
}
}
// Convenience methods
trace(message, data) { this.log('trace', message, data); }
debug(message, data) { this.log('debug', message, data); }
info(message, data) { this.log('info', message, data); }
warn(message, data) { this.log('warn', message, data); }
error(message, data) { this.log('error', message, data); }
fatal(message, data) { this.log('fatal', message, data); }
/**
* Protocol-specific logging methods
*/
logProtocol(direction, deviceId, packet, timing) {
if (!this.shouldLog('debug')) return;
this.debug(`Protocol ${direction}`, {
category: 'protocol',
deviceId,
packet: {
instruction: packet.instruction,
parameters: Array.from(packet.parameters || []),
length: packet.data?.length || 0
},
timing
});
}
logPacketSent(deviceId, instruction, parameters, timing) {
this.logProtocol('TX', deviceId, { instruction, parameters }, timing);
}
logPacketReceived(deviceId, packet, timing) {
this.logProtocol('RX', deviceId, packet, timing);
}
/**
* Connection logging
*/
logConnection(event, connectionType, details = {}) {
const level = event === 'error' ? 'error' : 'info';
this.log(level, `Connection ${event}`, {
category: 'connection',
connectionType,
...details
});
}
/**
* Device discovery logging
*/
logDiscovery(event, details = {}) {
this.info(`Discovery ${event}`, {
category: 'discovery',
...details
});
}
/**
* Performance tracking
*/
startPerformanceTimer(operation, context = {}) {
const timerId = `${operation}_${Date.now()}_${Math.random()}`;
this.performanceMetrics.set(timerId, {
operation,
context,
startTime: performance.now(),
startTimestamp: Date.now()
});
return timerId;
}
endPerformanceTimer(timerId, additionalData = {}) {
const metric = this.performanceMetrics.get(timerId);
if (!metric) return null;
const endTime = performance.now();
const duration = endTime - metric.startTime;
const performanceData = {
operation: metric.operation,
duration: Math.round(duration * 100) / 100, // Round to 2 decimal places
context: metric.context,
...additionalData
};
this.performanceMetrics.delete(timerId);
if (this.enablePerformanceMetrics) {
this.debug(`Performance: ${metric.operation}`, {
category: 'performance',
performance: performanceData
});
}
this.emit('performance', performanceData);
return performanceData;
}
/**
* Measure function execution time
*/
async measureAsync(operation, fn, context = {}) {
const timerId = this.startPerformanceTimer(operation, context);
try {
const result = await fn();
this.endPerformanceTimer(timerId, { success: true });
return result;
} catch (error) {
this.endPerformanceTimer(timerId, { success: false, error: error.message });
throw error;
}
}
measure(operation, fn, context = {}) {
const timerId = this.startPerformanceTimer(operation, context);
try {
const result = fn();
this.endPerformanceTimer(timerId, { success: true });
return result;
} catch (error) {
this.endPerformanceTimer(timerId, { success: false, error: error.message });
throw error;
}
}
/**
* Get filtered logs
*/
getLogs(filters = {}) {
let filteredLogs = [...this.logs];
if (filters.level) {
const minLevel = this.logLevels[filters.level];
filteredLogs = filteredLogs.filter(log => this.logLevels[log.level] >= minLevel);
}
if (filters.category) {
filteredLogs = filteredLogs.filter(log => log.category === filters.category);
}
if (filters.deviceId) {
filteredLogs = filteredLogs.filter(log => log.deviceId === filters.deviceId);
}
if (filters.since) {
filteredLogs = filteredLogs.filter(log => log.timestamp >= filters.since);
}
if (filters.limit) {
filteredLogs = filteredLogs.slice(-filters.limit);
}
return filteredLogs;
}
/**
* Get log statistics
*/
getStatistics() {
const categories = {};
const devices = {};
const errors = [];
for (const log of this.logs) {
// Count by category
categories[log.category] = (categories[log.category] || 0) + 1;
// Count by device
if (log.deviceId) {
devices[log.deviceId] = (devices[log.deviceId] || 0) + 1;
}
// Collect errors
if (log.level === 'error' || log.level === 'fatal') {
errors.push(log);
}
}
return {
totalLogs: this.logs.length,
logCounts: { ...this.logCount },
categories,
devices,
recentErrors: errors.slice(-10),
timeSpan: this.logs.length > 0 ? {
start: this.logs[0].timestamp,
end: this.logs[this.logs.length - 1].timestamp
} : null
};
}
/**
* Export logs
*/
exportLogs(format = 'json', filters = {}) {
const logs = this.getLogs(filters);
switch (format) {
case 'json':
return JSON.stringify(logs, null, 2);
case 'csv': {
if (logs.length === 0) return '';
const headers = ['timestamp', 'level', 'category', 'deviceId', 'message'];
const csvLines = [headers.join(',')];
for (const log of logs) {
const row = [
new Date(log.timestamp).toISOString(),
log.level,
log.category || '',
log.deviceId || '',
`"${log.message.replace(/"/g, '""')}"`
];
csvLines.push(row.join(','));
}
return csvLines.join('\n');
}
case 'text':
return logs.map(log => this.formatters.console(log)).join('\n');
default:
throw new Error(`Unsupported export format: ${format}`);
}
}
/**
* Clear logs
*/
clearLogs() {
this.logs = [];
this.logCount = { trace: 0, debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
this.emit('logs_cleared');
}
/**
* Set log level
*/
setLevel(level) {
if (!(level in this.logLevels)) {
throw new Error(`Invalid log level: ${level}`);
}
this.level = level;
this.emit('level_changed', level);
}
/**
* Create a child logger with context
*/
child(context = {}) {
const childLogger = Object.create(this);
childLogger.defaultContext = { ...this.defaultContext, ...context };
// Override log method to include default context
childLogger.log = (level, message, data = {}) => {
const mergedData = { ...childLogger.defaultContext, ...data };
return this.log(level, message, mergedData);
};
return childLogger;
}
/**
* Device-specific logger
*/
forDevice(deviceId) {
return this.child({ deviceId });
}
/**
* Category-specific logger
*/
forCategory(category) {
return this.child({ category });
}
}
// Global logger instance
export const logger = new Logger({
level: process.env.DYNAMIXEL_LOG_LEVEL || 'info',
enablePerformanceMetrics: process.env.DYNAMIXEL_PERFORMANCE_LOGGING === 'true'
});
// Export convenience methods
export const trace = logger.trace.bind(logger);
export const debug = logger.debug.bind(logger);
export const info = logger.info.bind(logger);
export const warn = logger.warn.bind(logger);
export const error = logger.error.bind(logger);
export const fatal = logger.fatal.bind(logger);