Files
bodyshop/server/utils/logger.js
2024-11-14 10:55:14 -08:00

228 lines
7.3 KiB
JavaScript

// Load environment variables THIS MUST BE AT THE TOP
const path = require("path");
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const InstanceManager = require("../utils/instanceMgr").default;
const winston = require("winston");
const WinstonCloudWatch = require("winston-cloudwatch");
const { isString, isEmpty } = require("lodash");
const { networkInterfaces, hostname } = require("node:os");
const { uploadFileToS3 } = require("./s3");
const { v4 } = require("uuid");
const { InstanceRegion } = require("./instanceMgr");
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 (logEntry.hasOwnProperty(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 getHostNameOrIP = () => {
// Try to get the hostname first
const hostName = hostname();
if (hostName) return hostName;
const interfaces = networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (iface.family === "IPv4" && !iface.internal) {
return iface.address;
}
}
}
return "127.0.0.1";
};
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);
};
return {
log,
logger: winstonLogger
};
} 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();