// Load environment variables THIS MUST BE AT THE TOP const InstanceManager = require("../utils/instanceMgr").default; const winston = require("winston"); const WinstonCloudWatch = require("winston-cloudwatch"); const { isString, isEmpty } = require("lodash"); const { uploadFileToS3 } = require("./s3"); const { v4 } = require("uuid"); const { InstanceRegion } = require("./instanceMgr"); const getHostNameOrIP = require("./getHostNameOrIP"); const client = require("../graphql-client/graphql-client").client; const queries = require("../graphql-client/queries"); const LOG_LEVELS = { error: { level: 0, name: "error" }, warn: { level: 1, name: "warn" }, info: { level: 2, name: "info" }, http: { level: 3, name: "http" }, verbose: { level: 4, name: "verbose" }, debug: { level: 5, name: "debug" }, silly: { level: 6, name: "silly" } }; const LOG_LENGTH_LIMIT = 256 * 1024; // 256KB const S3_BUCKET_NAME = InstanceManager({ imex: "imex-large-log", rome: "rome-large-log" }); const region = InstanceRegion(); const estimateLogSize = (logEntry) => { let estimatedSize = 0; for (const key in logEntry) { if (Object.prototype.hasOwnProperty.call(logEntry, key)) { const value = logEntry[key]; if (value === undefined || value === null) { estimatedSize += key.length; // Only count the key length if value is undefined or null } else { estimatedSize += key.length + (typeof value === "string" ? value.length : JSON.stringify(value).length); } } } return estimatedSize; }; const normalizeLevel = (level) => (level ? level.toLowerCase() : LOG_LEVELS.debug.name); const createLogger = () => { try { const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); const logGroupName = isLocal ? "development" : process.env.CLOUDWATCH_LOG_GROUP; const winstonCloudwatchTransportDefaults = { logGroupName: logGroupName, awsOptions: { region }, jsonMessage: true }; if (isLocal) { winstonCloudwatchTransportDefaults.awsOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; } const levelFilter = (levels) => { return winston.format((info) => { if (Array.isArray(levels)) { return levels.includes(info.level) ? info : false; } else { return info.level === levels ? info : false; } })(); }; const createProductionTransport = (level, logStreamName, filters) => { return new WinstonCloudWatch({ level, logStreamName: logStreamName || level, format: levelFilter(filters || level), ...winstonCloudwatchTransportDefaults }); }; const internalHostname = process.env.HOSTNAME || getHostNameOrIP(); const getDevelopmentTransports = () => [ new winston.transports.Console({ level: "silly", format: winston.format.combine( winston.format.colorize(), winston.format.timestamp(), winston.format.printf(({ level, message, timestamp, user, record, meta }) => { const hostnameColor = `\x1b[34m${internalHostname}\x1b[0m`; // Blue const timestampColor = `\x1b[36m${timestamp}\x1b[0m`; // Cyan const labelColor = "\x1b[33m"; // Yellow const separatorColor = "\x1b[35m|\x1b[0m"; // Magenta for separators return `${timestampColor} [${hostnameColor}] [${level}]: ${message} ${ user ? `${separatorColor} ${labelColor}user:\x1b[0m ${JSON.stringify(user)}` : "" } ${record ? `${separatorColor} ${labelColor}record:\x1b[0m ${JSON.stringify(record)}` : ""}${ meta ? `\n${separatorColor} ${labelColor}meta:\x1b[0m ${JSON.stringify(meta, null, 2)} ${separatorColor}` : "" }`; }) ) }) ]; const getProductionTransports = () => [ createProductionTransport("error"), createProductionTransport("warn"), createProductionTransport("info"), createProductionTransport("silly", "debug", ["http", "verbose", "debug", "silly"]) ]; const winstonLogger = winston.createLogger({ format: winston.format.json(), transports: process.env.NODE_ENV === "production" ? getProductionTransports() : [...getDevelopmentTransports(), ...getProductionTransports()] }); if (isLocal) { winstonLogger.debug( `CloudWatch set to LocalStack end point: ${winstonCloudwatchTransportDefaults.awsOptions.endpoint}` ); } const log = (message, type, user, record, meta, upload) => { const logEntry = { level: normalizeLevel(type), message, user, record, hostname: internalHostname, meta }; const uploadLogToS3 = (logEntry, message, type, user) => { const uniqueId = v4(); const dateTimeString = new Date().toISOString().replace(/:/g, "-"); const envName = process.env?.NODE_ENV ? process.env.NODE_ENV : ""; const logStreamName = `${envName}-${internalHostname}-${dateTimeString}-${uniqueId}.json`; const logString = JSON.stringify(logEntry); const webPath = isLocal ? `https://${S3_BUCKET_NAME}.s3.localhost.localstack.cloud:4566/${logStreamName}` : `https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/${logStreamName}`; uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: logStreamName, content: logString }) .then(() => { log("A log file has been uploaded to S3", "info", "S3", null, { logStreamName, webPath, message: message?.slice(0, 200), type, user }); }) .catch((err) => { log("Error in S3 Upload", "error", "S3", null, { logStreamName, webPath, message: message?.slice(0, 100), type, user, errorMessage: err?.message?.slice(0, 100) }); }); }; const checkAndUploadLog = () => { const estimatedSize = estimateLogSize(logEntry); if (estimatedSize > LOG_LENGTH_LIMIT * 0.9 || estimatedSize > LOG_LENGTH_LIMIT) { uploadLogToS3(logEntry, message, type, user); return true; } return false; }; // Upload log immediately if upload is true, otherwise check the log size. if (upload) { uploadLogToS3(logEntry, message, type, user); return; } if (checkAndUploadLog()) return; winstonLogger.log(logEntry); }; const LogIntegrationCall = async ({ platform, method, name, jobid, paymentid, billid, status, bodyshopid, email }) => { try { //Insert the record. await client.request(queries.INSERT_INTEGRATION_LOG, { log: { platform, method, name, jobid, paymentid, billid, status: status?.toString() ?? "0", bodyshopid, email } }); } catch (error) { console.trace("Stack", error?.stack); log("integration-log-error", "ERROR", email, null, { message: error?.message, stack: error?.stack, platform, method, name, jobid, paymentid, billid, status, bodyshopid, email }); } }; return { log, logger: winstonLogger, LogIntegrationCall }; } catch (e) { console.error("Error setting up enhanced Logger, defaulting to console.: " + e?.message || ""); return { log: console.log, logger: console.log, LOG_LEVELS }; } }; module.exports = createLogger();