@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
JavaScript
// 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;