tcp-serial-relay
Version:
Modular TCP to Serial relay service with comprehensive logging, monitoring and web dashboard
305 lines (271 loc) • 8.53 kB
JavaScript
// src/utils/logger.js
const winston = require('winston');
const path = require('path');
const fs = require('fs');
const https = require('https');
require('winston-daily-rotate-file');
const { getDeviceId } = require('./device-info');
class Logger {
constructor() {
this.logDir = path.join(process.cwd(), 'logs');
this.setupLogDirectory();
this.createLoggers();
this.collectLogs = false;
this.logBuffer = [];
this.maxBufferSize = 1000; // Limit buffer size to prevent memory issues
}
setupLogDirectory() {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
}
createLoggers() {
// Create a custom format that also captures logs for the buffer if collectLogs is enabled
const captureFormat = winston.format((info) => {
if (this.collectLogs) {
// Store log entry in buffer for later posting
if (this.logBuffer.length < this.maxBufferSize) {
this.logBuffer.push({
timestamp: info.timestamp || new Date().toISOString(),
level: (info.level || 'info').toString().toLowerCase(),
message: info.message || '',
additionalData: { ...info, timestamp: undefined, level: undefined, message: undefined }
});
}
}
return info;
});
// Main application logger
this.appLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss.SSS'
}),
winston.format.errors({ stack: true }),
captureFormat(),
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
// Ensure level is a string and handle potential undefined/null values
const levelStr = (level || 'info').toString().toUpperCase();
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
if (stack) {
return `${timestamp} [${levelStr}]: ${message}\n${stack}${metaStr}`;
}
return `${timestamp} [${levelStr}]: ${message}${metaStr}`;
})
),
transports: this.createTransports(),
exceptionHandlers: this.createExceptionHandlers(),
rejectionHandlers: this.createRejectionHandlers()
});
// Data transfer logger for high-frequency logs
this.dataLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ timestamp, message }) => `${timestamp} ${message}`)
),
transports: [
new winston.transports.DailyRotateFile({
filename: path.join(this.logDir, 'data-transfer-%DATE%.log'),
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: '50m',
maxFiles: '7d'
})
]
});
this.appLogger.info('Logger initialized', {
logDir: this.logDir,
logLevel: this.appLogger.level,
nodeEnv: process.env.NODE_ENV || 'development'
});
}
createTransports() {
const transports = [];
// Console transport
if (process.env.NODE_ENV !== 'production') {
transports.push(
new winston.transports.Console({
level: 'info',
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message }) => {
const levelStr = (level || 'info').toString();
return `${timestamp} ${levelStr}: ${message}`;
})
)
})
);
}
// File transports
transports.push(
// Main log file
new winston.transports.DailyRotateFile({
filename: path.join(this.logDir, 'app-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
level: 'info'
}),
// Error-only log file
new winston.transports.DailyRotateFile({
filename: path.join(this.logDir, 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '10m',
maxFiles: '30d',
level: 'info'
})
);
return transports;
}
createExceptionHandlers() {
return [
new winston.transports.DailyRotateFile({
filename: path.join(this.logDir, 'exceptions-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '10m',
maxFiles: '30d'
})
];
}
createRejectionHandlers() {
return [
new winston.transports.DailyRotateFile({
filename: path.join(this.logDir, 'rejections-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '10m',
maxFiles: '30d'
})
];
}
// Public methods to get loggers
getAppLogger() {
return this.appLogger;
}
getDataLogger() {
return this.dataLogger;
}
// Utility method for safe object stringification
safeStringify(obj, maxDepth = 3) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, val) => {
if (val != null && typeof val === 'object') {
if (seen.has(val)) return '[Circular]';
seen.add(val);
}
return val;
}, 2);
}
/**
* Enable log collection
* @param {boolean} collect - Whether to collect logs
*/
setCollectLogs(collect) {
this.collectLogs = collect;
this.appLogger.info(`Log collection ${collect ? 'enabled' : 'disabled'}`);
if (!collect) {
// Clear buffer if collection is disabled
this.clearLogBuffer();
}
}
/**
* Clear the log buffer
*/
clearLogBuffer() {
const count = this.logBuffer.length;
this.logBuffer = [];
this.appLogger.debug(`Cleared log buffer (${count} entries)`);
}
/**
* Get the current log buffer
* @returns {Array} Array of log entries
*/
getLogBuffer() {
return [...this.logBuffer];
}
/**
* Post logs to the specified endpoint
* @returns {Promise<boolean>} Success status
*/
async postLogs() {
if (!this.collectLogs || this.logBuffer.length === 0) {
this.appLogger.info('No logs to post to endpoint');
return false;
}
const deviceId = getDeviceId();
return new Promise((resolve) => {
const postData = JSON.stringify({
deviceId,
logs: this.logBuffer
});
const options = {
hostname: 'logs-2lbtz4kjxa-uc.a.run.app',
path: '/',
port: 443,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': postData.length
}
};
this.appLogger.info('Posting logs to endpoint', {
endpoint: 'https://logs-2lbtz4kjxa-uc.a.run.app',
deviceId,
logCount: this.logBuffer.length
});
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (res.statusCode === 200 || res.statusCode === 201) {
this.appLogger.info('Logs successfully posted to endpoint', {
statusCode: res.statusCode,
response: responseData.substring(0, 100) // Log only first 100 chars
});
this.clearLogBuffer();
resolve(true);
} else {
this.appLogger.warn('Failed to post logs to endpoint', {
statusCode: res.statusCode,
response: responseData.substring(0, 100)
});
resolve(false);
}
});
});
req.on('error', (error) => {
this.appLogger.error('Error posting logs to endpoint', { error: error.message });
resolve(false);
});
req.setTimeout(10000, () => {
this.appLogger.warn('Log posting request timed out');
req.abort();
resolve(false);
});
req.write(postData);
req.end();
});
}
// Graceful shutdown
async close() {
return new Promise((resolve) => {
this.appLogger.on('finish', resolve);
this.appLogger.end();
});
}
}
// Singleton instance
const loggerInstance = new Logger();
module.exports = {
logger: loggerInstance.getAppLogger(),
dataLogger: loggerInstance.getDataLogger(),
loggerInstance,
safeStringify: loggerInstance.safeStringify.bind(loggerInstance)
};