UNPKG

@bridgera-iot/bridgera-structured-logger

Version:

A lightweight structured logger for Node.js with JSON output to console and file, supporting custom transports.

167 lines (148 loc) 5.33 kB
// index.js const path = require("path"); const nodemailer = require("nodemailer"); class StructuredLogger { /** * @typedef {Object} EmailOptions * @property {Object} transport Nodemailer transport options * @property {string} from Sender address * @property {string} to Recipient address * @property {("error"|"warn"|"info")} [level="error"] Threshold for sending emails * @property {string} [subjectTemplate] Default subject template: "{{message}}" */ /** * @param {string} serviceName Identifier for your service * @param {("development"|"production")} env * @param {EmailOptions} [emailOptions] Optional email configuration */ constructor( serviceName = "default-service", env = "development", emailOptions = null ) { this.serviceName = serviceName; this.env = ["dev", "development", "qa", "local"].includes(env) ? "development" : env; console.log( `[StructuredLogger] Initialized for service: ${this.serviceName} in ${this.env} mode` ); //console.log("[StructuredLogger] Email options:", emailOptions); if (emailOptions) { // you can uncomment these to see SMTP logs: //emailOptions.transport.logger = true; //emailOptions.transport.debug = true; this.transporter = nodemailer.createTransport(emailOptions.transport); this.mailFrom = emailOptions.from; this.mailTo = emailOptions.to; this.emailLevel = emailOptions.level || "error"; this.subjectTemplate = emailOptions.subjectTemplate || "{{message}}"; // severity order this.levels = ["error", "warn", "info", "debug", "trace"]; } } /** * Return the calling function name and file name, * skipping any internal/node_modules frames. * * @returns {{ functionName: string, fileName: string }} */ _getCaller() { const origPrepare = Error.prepareStackTrace; Error.prepareStackTrace = (_, stack) => stack; const err = new Error(); Error.captureStackTrace(err, this._getCaller); const stack = err.stack; Error.prepareStackTrace = origPrepare; const projectRoot = process.cwd(); for (const frame of stack) { const fullPath = frame.getFileName(); if (!fullPath) continue; // 1) skip node_modules if (fullPath.includes(`${path.sep}node_modules${path.sep}`)) continue; // 2) skip this logger module if (fullPath.startsWith(__dirname)) continue; // 3) skip your wrapper file if named logger-setup.js if (fullPath.endsWith(`${path.sep}logger-setup.js`)) continue; const fn = frame.getFunctionName() || frame.getMethodName() || "<anonymous>"; const relFile = path.relative(projectRoot, fullPath); return { functionName: fn, fileName: relFile }; } return { functionName: "<anonymous>", fileName: "<unknown>" }; } log(level, message, meta = {}) { const { functionName, fileName } = this._getCaller(); const entry = { timestamp: new Date().toISOString(), level, message, service: this.serviceName, function: functionName, file: fileName, ...(typeof meta === "object" && meta !== null ? meta : { meta }), }; // Console output const output = this.env === "production" ? JSON.stringify(entry) : JSON.stringify(entry, null, 2); console.log(output); // Decide whether to send email if (this.transporter) { const skipEmail = meta.sendEmail === false; const viaThreshold = this._shouldEmail(level); const forceEmail = meta.sendEmail === true; const isCritical = meta.critical === true; // console.log("[StructuredLogger] Email conditions:", { // skipEmail, // viaThreshold, // forceEmail, // isCritical, // }); if (!skipEmail && (viaThreshold || forceEmail || isCritical)) { // Build subject with tokens const subject = this.subjectTemplate .replace("{{service}}", this.serviceName) .replace("{{level}}", level) .replace("{{message}}", message); const mailOpts = { from: this.mailFrom, to: this.mailTo, subject, text: JSON.stringify(entry, null, 2), html: `<pre>${JSON.stringify(entry, null, 2)}</pre>`, }; // console.log("[StructuredLogger] Checking email conditions..."); // console.log("[StructuredLogger] sendEmail:", meta.sendEmail); // console.log( // "[StructuredLogger] level:", // level, // " | threshold:", // this.emailLevel // ); this.transporter .sendMail(mailOpts) .then((info) => console.log("[StructuredLogger] Email sent:", info)) .catch((err) => console.error("[StructuredLogger] Email failed:", err) ); } } } info(msg, meta) { this.log("info", msg, meta); } warn(msg, meta) { this.log("warn", msg, meta); } error(msg, meta) { this.log("error", msg, meta); } _shouldEmail(level) { const thresholdIdx = this.levels.indexOf(this.emailLevel); const levelIdx = this.levels.indexOf(level); return levelIdx <= thresholdIdx; } } module.exports = StructuredLogger;