@rollercoaster-dev/rd-logger
Version:
A neurodivergent-friendly logger for Rollercoaster.dev projects
152 lines (151 loc) • 5.53 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import fs from 'fs';
import path from 'path';
import { safeStringify } from '../utils';
/**
* File transport for the logger
*/
export class FileTransport {
constructor(options) {
this.name = 'file';
this.initialized = false;
this.fileStream = null;
this.logQueue = [];
this.isProcessingQueue = false;
this.filePath = options.filePath;
}
/**
* Initialize the file transport (called lazily)
*/
initialize() {
try {
this.ensureLogDirectoryExists();
this.initializeFileStream();
this.initialized = true;
}
catch (error) {
console.error(`Failed to initialize file transport: ${error.message}`);
}
}
/**
* Log a message to the file
*/
log(level, message, timestamp, context) {
// Initialize lazily on first log attempt
if (!this.initialized) {
this.initialize();
// If initialization failed, fileStream will be null, and we can't log
if (!this.initialized) {
console.warn('FileTransport: Cannot log, initialization failed.');
return;
}
}
// Create a plain text version for the file - always use ISO format for machine readability
let fileEntry = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
if (Object.keys(context).length) {
fileEntry += ` | ${safeStringify(context)}`;
}
// Add to queue and process asynchronously
this.logQueue.push(fileEntry);
this.processLogQueue();
}
/**
* Clean up resources used by the transport
*/
cleanup() {
if (this.fileStream) {
// Process any remaining logs in the queue
if (this.logQueue.length > 0) {
for (const entry of this.logQueue) {
this.fileStream.write(entry + '\n');
}
this.logQueue = [];
}
// Close the file stream
this.fileStream.end();
this.fileStream = null;
}
}
/**
* Ensure the log directory exists
*/
ensureLogDirectoryExists() {
const logDir = path.dirname(this.filePath);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
}
/**
* Initialize the file write stream
*/
initializeFileStream() {
if (this.fileStream) {
this.fileStream.end();
this.fileStream = null;
}
try {
this.fileStream = fs.createWriteStream(this.filePath, { flags: 'a' });
// Handle stream errors
this.fileStream.on('error', (error) => {
console.error(`Error writing to log file: ${error.message}`);
this.fileStream = null;
});
// Process any queued logs once the stream is ready
this.fileStream.on('ready', () => {
this.processLogQueue();
});
}
catch (error) {
console.error(`Failed to create write stream for '${this.filePath}': ${error.message}`);
}
}
/**
* Process the log queue asynchronously
*/
processLogQueue() {
return __awaiter(this, void 0, void 0, function* () {
if (this.isProcessingQueue || this.logQueue.length === 0 || !this.fileStream) {
return;
}
this.isProcessingQueue = true;
try {
while (this.logQueue.length > 0) {
const entry = this.logQueue.shift();
if (entry && this.fileStream) {
// Use a promise to handle backpressure
const canContinue = this.fileStream.write(entry + '\n');
if (!canContinue) {
// Wait for drain event before continuing
yield new Promise((resolve) => {
if (this.fileStream) {
this.fileStream.once('drain', resolve);
}
else {
resolve();
}
});
}
}
}
}
catch (error) {
console.error(`Error processing log queue: ${error.message}`);
}
finally {
this.isProcessingQueue = false;
// Check if more entries were added while processing
if (this.logQueue.length > 0) {
this.processLogQueue();
}
}
});
}
}