mcode-log
Version:
A stand-alone package of core code for logging and debugging.
1,248 lines (1,130 loc) • 64.6 kB
JavaScript
// #region F I L E
// <copyright file="mcode-log/index.js" company="MicroCODE Incorporated">Copyright © 2022-2024 MicroCODE, Inc. Troy, MI</copyright><author>Timothy J. McGuire</author>
// #region M O D U L E
// #region D O C U M E N T A T I O N
/**
* Project: MicroCODE MERN Applications
* Customer: Internal + MIT xPRO Course
* @module 'mcode-log.js'
* @memberof mcode
* @created January 2022-2024
* @author Timothy McGuire, MicroCODE, Inc.
* @description >
* MicroCODE Shared App Logging Library
*
* LICENSE:
* --------
* MIT License: MicroCODE.mcode-log
*
* Copyright (c) 2022-2024 Timothy McGuire, MicroCODE, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*
* DESCRIPTION:
* ------------
* This module implements the MicroCODE's Common JavaScript functions for logging and debugging.
*
*
* REFERENCES:
* -----------
* 1. MIT xPRO Course: Professional Certificate in Coding: Full Stack Development with MERN
*
* 2. List of ANSI Color Escape Sequences
* https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
*
* 3. Showing Line Numbers in console.log from Node.js
* https://stackoverflow.com/questions/45395369/how-to-get-console-log-line-numbers-shown-in-nodejs
*
*
*
*
* MODIFICATIONS:
* --------------
* Date: By-Group: Rev: Description:
*
* 27-Jan-2022 TJM-MCODE {0001} New module for common reusable Javascript logging functions.
* 05-Mar-2022 TJM-MCODE {0002} Documentation updates.
* 04-May-0222 TJM-MCODE {0003} Corrected 'month' in timeStamp.
* 03-Oct-2022 TJM-MCODE {0004} Added 'log()' to simplify console logging of app events.
* 03-Oct-2022 TJM-MCODE {0005} Added use of 'vt' for colorizing Console Log entries.
* 16-Oct-2022 TJM-MCODE {0006} Added 'success' as a severity.
* 30-Oct-2023 TJM-MCODE {0007} Updated to TypeScript, reversed to pure JavaScript in Jan 2024.
* 03-Dec-2023 TJM-MCODE {0008} Don't log 'debug' messages in staging or production mode.
* 21-Jan-2024 TJM-MCODE {0009} Converted to a single ES6 Module (ESM) for use in both
* Frontend/Client and Backend/Server as a NodeJS package.
* 01-Feb-2024 TJM-MCODE {0010} Changed to the Universal Module Definition (UMD) pattern to support AMD,
* CommonJS/Node.js, and browser global in our exported module.
* 02-Mar-2024 TJM-MCODE {0011} Added 'logobj()', 'expobj()', 'isFunction()', 'hexify()', 'octify()', and 'colorizeLines()'
* all in the pursuit of a more complete and consistent logging and debugging experience,
* in both the Console, NPM, and the Browser's DevTools.
* 06-Jul-2024 TJM-MCODE {0012} 0.4.00 - moved all 'data' functions into sub-package 'mcode-data'.
* 22-Aug-2024 TJM-MCODE {0013} 0.4.04 - corrected 'colorizeLines()' to carry on embedded colors to following lines.
* 22-Aug-2024 TJM-MCODE {0014} 0.4.05 - corrected 'logify()' to accept all legal JSON Key names.
* 19-Feb-2025 TJM-MCODE {0015} 0.5.08 - updated 'resx()' to support returning non-db entity results,
* to carry this common response code into our HTMX UI responses.
* 21-Feb-2025 TJM-MCODE {0016} 0.5.09 - optimized many functions, standardized on '' strings instead of a mix
* of "" and '', now "" only used when embedded ' are needed.
* - fixed an issues in 'logify*()' with string arrays where element had embedded ".
* 08-Mar-2025 TJM-MCODE {0017} 0.5.10 - updated resx() handle HTTP Status 204 properly with '.end()'.
* 16-Mar-2025 TJM-MCODE {0018} 0.6.07 - Passing MODULE_NAME is now optional and the logging functions
* all log complete source path and line # of the caller automatically.
*
*
*
* NOTE: This module follow's MicroCODE's JavaScript Style Guide and Template JS file, see:
*
* o https://github.com/MicroCODEIncorporated/JavaScriptSG
* o https://github.com/MicroCODEIncorporated/TemplatesJS
*
* ...be sure to check out the CTRL-SHIFT+K, +L, +J keybaord shortcuts in Visual Studio Code
* for taking advance of the #regions in this file and our templates.
*
*
*/
// #endregion
// #region I M P O R T S
const path = require("path");
const data = require('mcode-data');
const packageJson = require('./package.json');
// #endregion
// #region T Y P E S
// #endregion
// #region I N T E R F A C E S
// #endregion
// #region C O N S T A N T S, F U N C T I O N S – P U B L I C
// MicroCODE: define this module's name for our 'mcode-log' package
const MODULE_NAME = 'mcode-log.js';
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const theme = process.env.THEME || 'dark'; // default to dark mode
const mode = process.env.NODE_ENV || 'development'; // default to development mode
/**
* @namespace mcode
* @desc mcode namespace containing functions and constants.
*/
const mcode = {
/**
* @const vt
* @memberof mcode
* @desc Colors constants for changing Console appearance ala DEC's VT52 + VT100 + VT220.
* @example
ANSI Color Escape Sequence
\x1b[***m -- where '***' is a series of command codes separated by semi-colons (;).
Code Effect -- notes
------------------------------------------------------------------------------
0 Reset / Normal -- all attributes off
1 Bold -- increased intensity
2 Faint -- decreased intensity - not widely supported
3 Italic -- not widely supported, sometimes treated as inverse
4 Underline
5 Slow Blink -- less than 150 per minute
6 Rapid Blink -- MS-DOS ANSI.SYS; 150+ per minute; not widely supported
7 Reverse video -- swap foreground and background colors
8 Conceal -- not widely supported.
9 Crossed-out -- characters legible, but marked for deletion. Not widely supported
10 Primary font (default)
11–19 Alternate font -- select alternate font n-10
20 Fraktur -- hardly ever supported
21 Bold off or Double Underline -- bold off not widely supported; double underline hardly ever supported
22 Normal color or intensity -- neither bold nor faint
23 Not italic, not Fraktur
24 Underline roundOff -- not singly or doubly underlined
25 Blink off
27 Inverse off
28 Reveal conceal off
29 Not crossed out
30–37 Set foreground color -- see color table below
38 Set foreground color -- next arguments are 5;<n> or 2;<r>;<g>;<b>, see below
39 Default foreground color -- implementation defined (according to standard)
40–47 Set background color -- see color table below
48 Set background color -- next arguments are 5;<n> or 2;<r>;<g>;<b>, see below
49 Default background color -- implementation defined (according to standard)
51 Framed
52 Encircled
53 Overlined
54 Not framed or encircled
55 Not overlined
60 ideogram underline -- hardly ever supported
61 ideogram double underline -- hardly ever supported
62 ideogram overline -- hardly ever supported
63 ideogram double overline -- hardly ever supported
64 ideogram stress marking hardly ever supported
65 ideogram attributes off reset the effects of all of 60-64
90–97 Set bright foreground color aixterm (not in standard)
100–107 Set bright background color aixterm (not in standard)
*
*/
vt:
{
notice: "This is a test string for logifying 'mcode' as an object during testing.",
// common effects, predefined ANSI escape sequences
reset: '\x1b[0m',
bold: '\x1b[1m',
bright: '\x1b[1m',
dim: '\x1b[2m',
faint: '\x1b[2m',
italic: '\x1b[3m',
underscore: '\x1b[4m',
underline: '\x1b[4m',
blink: '\x1b[5m',
blink_slow: '\x1b[5m',
blink_fast: '\x1b[6m',
reverse: '\x1b[7m',
hidden: '\x1b[8m',
conceal: '\x1b[8m',
strikethru: '\x1b[9m',
crossed_out: '\x1b[9m',
// foreground colors
fg: {
black: '\x1b[30m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
},
// background colors
bg: {
black: '\x1b[40m',
red: '\x1b[41m',
green: '\x1b[42m',
yellow: '\x1b[43m',
blue: '\x1b[44m',
magenta: '\x1b[45m',
cyan: '\x1b[46m',
white: '\x1b[47m',
},
// colors for event severity: dark light
gray: (theme === 'dark') ? '\x1b[90m' : '\x1b[30m', // gray
errr: (theme === 'dark') ? '\x1b[91m' : '\x1b[31m', // red
good: (theme === 'dark') ? '\x1b[92m' : '\x1b[32m', // green
warn: (theme === 'dark') ? '\x1b[93m' : '\x1b[33m', // yellow
cold: (theme === 'dark') ? '\x1b[94m' : '\x1b[34m', // blue
dead: (theme === 'dark') ? '\x1b[95m' : '\x1b[35m', // magenta
code: (theme === 'dark') ? '\x1b[96m' : '\x1b[36m', // cyan
info: (theme === 'dark') ? '\x1b[97m' : '\x1b[37m', // white
dbug: (theme === 'dark') ? '\x1b[97m' : '\x1b[37m', // white
// custom JSON colors -- see 'logify()' for use
key: '\x1b[96m', // key name - CYAN
value: '\x1b[93m', // number, boolean, null - YELLOW
string: '\x1b[94m', // string value - BLUE
},
/**
* @func extractEntity
* @memberof mcode
* @api private
* @desc Extracts, capitalizes, and returns the ENTITY name of a source module,
* by MicroCODE's convention this is APP of app.controller.js, USER of user.view.js, etc.
* @param {string} relativePath location of the source code module.
* @returns Just the first part--before 1st '.'--of the source file.
*/
extractEntity: function (relativePath)
{
// Extract the file name (last part of the path)
const parts = relativePath.split(/[\\/]/); // Split on both forward and backslashes
const fileName = parts.pop(); // Get last element (file name)
// Extract the part before the first dot (.), return ready for message header as [ENTITY]
return fileName.split('.')[0].toUpperCase();
},
/**
* @func getFrom
* @memberof mcode
* @api private
* @desc Helper function for log() to determine module and line of caller.
* @param source the module name from the mcode.log() call as a default (optional).
* @returns (2) strings: 'appModule' and 'module:line' of mcode.log() caller.
*/
getFrom: function (source)
{
// Get <app-base-dir> (3 levels up from <baseDir>/node_modules/mcode-log)
const baseDir = path.resolve(__dirname, "../../..");
let moduleLine = `${source}:???`;
let appModule = source.split(/[\.,:;!?\s]+/)[0].toUpperCase();
if (typeof Error.prepareStackTrace !== "function")
{
// Fallback for non-V8 engines -- crawl a string stack trace
const stack = new Error().stack.split("\n");
for (let i = 2; i < stack.length; i++)
{
if (!stack[i].includes("index.js"))
{
const match = stack[i].match(/\(([^)]+):(\d+):\d+\)/);
if (match)
{
// convert to relative path to keep short but informative
const filePath = match[1];
const lineNumber = match[2];
let relativePath = (filePath.includes('node:'))
? filePath
: path.relative(baseDir, filePath);
moduleLine = `${relativePath}:${lineNumber}`;
appModule = this.extractEntity(relativePath);;
}
}
}
}
else
{
// V8 Engine, use structured stack trace for speed, save the original handler (string generator)
const prepareStackTrace = Error.prepareStackTrace;
try
{
// Override to return structured stack trace
Error.prepareStackTrace = (_, stack) => stack;
const stack = new Error().stack;
if (stack && stack.length > 2)
{
const caller = stack.find((s) => !s.getFileName().includes("index.js"));
if (caller)
{
// convert to relative path to keep short but informative
const filePath = caller.getFileName();
const lineNumber = caller.getLineNumber();
let relativePath = (filePath.includes('node:'))
? filePath
: path.relative(baseDir, filePath);
moduleLine = `${relativePath}:${lineNumber}`;
appModule = this.extractEntity(relativePath);
}
}
}
finally
{
// ensure restoration no matter what
Error.prepareStackTrace = prepareStackTrace;
}
}
return [appModule, moduleLine];
},
/**
* @func log
* @memberof mcode
* @desc Logs App Events to the Console in a standardized format.
* @api public
* @param {object} message pre-formatted message to be logged.
* @param {string} source where the message orginated.
* @param {string} severity Event.Severity: 'info', 'warn', 'error', 'exception', and 'success'.
* @param {string} error [Optional] error message from another source.
* @returns {string} '{severity}: {message}' for display in UI.
*
* @example
* mcode.log('This is a test message.', 'myModule', 'info');
* mcode.log('object', object, 'myModule);
*/
log: function (message = '<no message>', source = '<unknown>.js', severity = 'debug', error = null)
{
// if 'source' is not a string containing '.js' or '.ts', log it as an object...
if (!data.isString(source) || (!source.includes('.js') && !source.includes('.ts')))
{
return mcode.logobj(message, source, severity);
}
let vt = mcode.vt;
let logText = []; // build the response as an array for speed
let status = `${severity}: ${message}`;
let logifiedMessage = '';
// do not log 'debug' messages in production mode - {0008}
if ((severity === 'debug') && (mode === 'production'))
{
return status;
}
// flatten the message object to strings for logging...
if (data.isArray(message))
{
logifiedMessage += `{array}\n${vt.code}[\n`;
// loop through the array and log each element...
message.forEach(element =>
{
logifiedMessage += mcode.colorizeLines(mcode.logify(mcode.logifyObject(element)), vt.code);
logifiedMessage += ',\n';
});
logifiedMessage += ']';
}
else if (data.isObject(message))
{
logifiedMessage = '\n' + mcode.logify(mcode.logifyObject(message));
}
else if (data.isJson(message))
{
logifiedMessage = '\n' + mcode.logify(mcode.logifyObject(message));
}
else if (data.isFunction(message))
{
logifiedMessage = '\n' + `${message}`;
}
else
{
logifiedMessage = message;
}
const [appModule, moduleLine] = this.getFrom(source);
let sevColor = vt.reset;
let sevText = severity;
logText.push(`${vt.reset}${vt.dim}++\n${vt.reset}${vt.dim}`);
switch (severity)
{
case 'i':
case 'inf':
case 'info':
sevText = 'info';
sevColor += vt.info;
logText.push(` i 「mcode」: ${sevColor}📣 [${appModule}] '${mcode.colorizeLines(logifiedMessage, sevColor)}'`);
break;
case 'w':
case 'wrn':
case 'warn':
case 'warning':
sevText = 'warn';
sevColor += vt.warn;
logText.push(` ! 「mcode」: ${sevColor}⚠️ [${appModule}] '${mcode.colorizeLines(logifiedMessage, sevColor)}'`);
break;
case 'e':
case 'err':
case 'error':
sevText = 'error';
sevColor += vt.errr;
logText.push(` x 「mcode」: ${sevColor}⛔ [${appModule}] '${mcode.colorizeLines(logifiedMessage, sevColor)}'`);
break;
case 'x':
case 'exp':
case 'crash':
case 'exception':
sevText = 'exception';
sevColor += vt.dead;
logText.push(` * 「mcode」: ${sevColor}💀 [${appModule}] '${mcode.colorizeLines(logifiedMessage, sevColor)}'`);
break;
case 's':
case 'ack':
case 'done':
case 'success':
sevText = 'success';
sevColor += vt.good;
logText.push(` ✓ 「mcode」: ${sevColor}✅ [${appModule}] '${mcode.colorizeLines(logifiedMessage, sevColor)}'`);
break;
case 'd':
case 'dbg':
case 'dbug':
case 'debug':
sevText = 'debug';
sevColor += vt.dbug;
logText.push(` µ 「mcode」: ${sevColor}🔍 [${appModule}] '${mcode.colorizeLines(logifiedMessage, sevColor)}'`);
break;
case '?':
default:
sevText = 'undefined';
sevColor += vt.code;
logText.push(` ? 「mcode」: ${sevColor}❓ [${appModule}] '${mcode.colorizeLines(logifiedMessage, sevColor)}'`);
break;
}
logText.push('\n');
let logifiedError = false;
if (error)
{
if (data.isObject(error))
{
logifiedError = mcode.logifyObject(error);
}
else if (data.isJson(error))
{
logifiedError = mcode.logifyObject(error);
}
else
{
logifiedError = error;
}
status += ` ERROR: ${mcode.simplify(logifiedError)}`;
}
if (logifiedError)
{
logifiedError = mcode.colorizeLines(logifiedError, sevColor);
logText.push(`${vt.reset}${vt.dim} error: ${vt.reset}${sevColor}${mcode.colorizeLines(mcode.simplify(logifiedError), sevColor)}\n`);
}
logText.push(`${vt.reset}${vt.dim} time: ${vt.reset}${mcode.timeStamp()}`);
logText.push(`${vt.reset}${vt.dim} from: ${vt.reset}${moduleLine}`);
logText.push(`${vt.reset}${vt.dim} severity: ${vt.reset}${sevColor}${sevText}${vt.reset}\n`);
logText.push(`${vt.reset}${vt.dim}--${vt.reset}`);
console.log(logText.join(''));
return status; // for caller to use as needed
},
/**
* @func logobj
* @memberof mcode
* @desc Logs a labeled Object to the Console in a standardized format.
* @api public
* @param {string} objName the name of the Object and/or a message to precede it in the log.
* @param {object} obj javaScript Object to log.
* @param {string} source where the Object orginated.
*
* @example
* mcode.logobj('myObject', myObject, 'myModule');
* mcode.obj('myObject', myObject, 'myModule');
*/
logobj: function (objName, obj, source = '<undefined>.js')
{
let vt = mcode.vt;
let logText = []; // build the response as an array for speed
let logifiedMessage = '';
// flatten the message object to strings for logging...
if (data.isArray(obj))
{
logifiedMessage += `{array}\n\n${vt.code}${objName}: \n[\n`;
// loop through the array and log each element...
obj.forEach(element =>
{
logifiedMessage += mcode.colorizeLines(mcode.logify(mcode.logifyObject(element)), vt.code);
logifiedMessage += ',\n';
});
logifiedMessage += ']';
}
else if (data.isObject(obj))
{
logifiedMessage = `{${(typeof obj)}}\n\n${vt.code}${objName}:\n` + mcode.colorizeLines(mcode.logify(mcode.logifyObject(obj)), vt.code);
}
else if (data.isJson(obj))
{
logifiedMessage = `{json}\n\n${vt.code}${objName}:\n` + mcode.colorizeLines(mcode.logify(mcode.logifyObject(obj)), vt.code);
}
else
{
logifiedMessage = `{${(typeof obj)}}\n\n${vt.code}${objName}: ${vt.info}` + obj;
}
const [appModule, moduleLine] = this.getFrom(source);
let sevColor = vt.reset;
let sevText = 'info';
sevColor += vt.info;
logText.push(`${vt.reset}${vt.dim}++\n`);
logText.push(`${vt.reset}${vt.dim} i 「mcode」: ${sevColor}📣 [${appModule}] '${mcode.colorizeLines(logifiedMessage, sevColor)}'\n`);
logText.push(`${vt.reset}${vt.dim} time: ${vt.reset}${mcode.timeStamp()}`);
logText.push(`${vt.reset}${vt.dim} from: ${vt.reset}${moduleLine}`);
logText.push(`${vt.reset}${vt.dim} severity: ${vt.reset}${sevColor}${sevText}${vt.reset}\n`);
logText.push(`${vt.reset}${vt.dim}--${vt.reset}`);
console.log(logText.join(''));
},
// convenient abbreviations of all the logged severities...
info: function (message, source) {mcode.log(message, source, 'info');},
warn: function (message, source) {mcode.log(message, source, 'warn');},
error: function (message, source) {mcode.log(message, source, 'error');},
error: function (message, source, error) {mcode.log(message, source, 'error', error);},
crash: function (message, source) {mcode.log(message, source, 'exception');},
done: function (message, source) {mcode.log(message, source, 'success');},
debug: function (message, source) {mcode.log(message, source, 'debug');},
success: function (message, source) {mcode.log(message, source, 'success');},
/**
* @func ready
* @memberof mcode
* @desc Logs a message to the Console when the module is loaded to show version.
*/
ready: function ()
{
this.log(`MicroCODE ${MODULE_NAME} v${packageJson.version} is loaded, mode: ${mode}, theme: ${theme}.`, MODULE_NAME, 'success');
},
/**
* @func exp
* @memberof mcode
* @desc logs an exception to the Console in a standardized format and a stack dump.
* @api public
* @param {object} message pre-formatted message to be logged.
* @param {string} source where the message orginated.
* @param {string} exception the underlying exception object/trace that was caught.
* @param {string} exptrace the underlying exception object/trace that was caught... if 'source' is an object to log.
* @returns {string} 'message: {message} - exception: {exception}' for display in UI.
*/
exp: function (message = '<no message>', source = '<unknown>.js', exception = {}, exptrace = {})
{
// if 'source' is not a string containing '.js' or '.ts' (or an API Route), log it as an object...
if (!data.isString(source) || (!source.includes('.js') && !source.includes('.ts') && !source.includes(`/`)))
{
return mcode.expobj(message, source, exception, exptrace);
}
let vt = mcode.vt;
let logText = []; // build the response as an array for speed
let logifiedMessage = '';
let logifiedException = '';
let isExpObject = false;
// flatten the message object to strings for logging...
if (data.isObject(message))
{
logifiedMessage = '\n' + mcode.logify(mcode.logifyObject(message));
}
else if (data.isJson(message))
{
logifiedMessage = '\n' + mcode.logify(mcode.logifyObject(message));
}
else
{
logifiedMessage = message;
}
// flatten the exception object to strings for logging...
if (data.isObject(exception))
{
isExpObject = true;
if (exception.stack)
{
// colorized the passed the stack trace...
logifiedException = mcode.colorizeLines(exception.stack, vt.gray);
}
else
{
// treat as an Object, not a stack trace and show in default colors...
logifiedException = `${vt.reset}` + mcode.colorizeLines(mcode.logify(mcode.logifyObject(exception)), vt.code);
}
}
else if (data.isJson(exception))
{
// treat as JSON, not a stack trace and show in default colors...
logifiedException = `${vt.reset}` + mcode.colorizeLines(mcode.logifyObject(exception), vt.code);
}
else
{
// treat as a string, not a stack trace or object and show in gray...
logifiedException = `${vt.reset}${vt.gray}` + exception;
logifiedException = mcode.colorizeLines(logifiedException, vt.gray);
}
const [appModule, moduleLine] = this.getFrom(source);
let sevColor = vt.reset;
sevColor += vt.dead;
// created a simplified exception message for the log entry...
const loggedException = ' exception: ' + mcode.colorizeLines(mcode.simplify(logifiedException), vt.dead);
// if 'loggedException' contains a stack trace, log it as an 'exception w/stack'
if (loggedException.includes('Error:') && loggedException.includes('at '))
{
isExpObject = true;
}
if (isExpObject)
{
logText.push(`${vt.reset}${vt.dim}++\n`);
logText.push(`${vt.reset}${vt.dim} * 「mcode」: ${sevColor}💀 [${appModule}] '${logifiedMessage}'\n`);
logText.push(`${vt.reset}${vt.dim}${sevColor} exception:\n`);
logText.push(logifiedException + `\n`);
logText.push(`${vt.reset}${vt.dim} time: ${vt.reset}${mcode.timeStamp()}`);
logText.push(`${vt.reset}${vt.dim} from: ${vt.reset}${moduleLine}`);
logText.push(`${vt.reset}${vt.dim} severity: ${sevColor}exception w/stack${vt.reset}\n`);
logText.push(`${vt.reset}${vt.dim}--${vt.reset}`);
console.log(logText.join(''));
return `${message} ${exception}`; // for caller to return
}
else
{
logText.push(`${vt.reset}${vt.dim}++\n`);
logText.push(`${vt.reset}${vt.dim} * 「mcode」: ${sevColor}💀 [${appModule}] '${logifiedMessage}'\n`);
logText.push(`${vt.reset}${vt.dim}${sevColor}${loggedException}${vt.gray}\n`);
logText.push(mcode.colorizeLines(`call stack: ${new Error().stack}\n`, vt.gray));
logText.push(`${vt.reset}${vt.dim} time: ${vt.reset}${mcode.timeStamp()}`);
logText.push(`${vt.reset}${vt.dim} from: ${vt.reset}${moduleLine}`);
logText.push(`${vt.reset}${vt.dim} severity: ${sevColor}exception w/trace${vt.reset}\n`);
logText.push(`${vt.reset}${vt.dim}--${vt.reset}`);
console.log(logText.join(''));
return `${message} ${exception}`; // for caller to return
}
},
/**
* @func expobj
* @memberof mcode
* @desc Logs a labeled Object to the Console in a standardized format, with an associated exception and stack dump.
* @api public
* @param {string} objName the name of the Object and/or a message to precede it in the log.
* @param {object} obj javaScript Object to log.
* @param {string} source where the Object orginated.
* @param {string} exception the underlying exception message that was caught.
* @returns {string} 'message: {message} - exception: {exception}' for display in UI.
*
* @example
* mcode.expobj('myObject', myObject, 'myModule', err); // from within a 'catch (err)' block
*/
expobj: function (objName = '<no name>', obj = {}, source = '<unknown>.js', exception = {})
{
let vt = mcode.vt;
let logText = []; // build the response as an array for speed
let logifiedMessage = '';
// flatten the message object to strings for logging...
if (data.isArray(obj))
{
logifiedMessage += `{array}\n\n${vt.code}${objName}: \n[\n`;
// loop through the array and log each element...
obj.forEach(element =>
{
logifiedMessage += mcode.colorizeLines(mcode.logify(mcode.logifyObject(element)), vt.code);
logifiedMessage += ',\n';
});
logifiedMessage += ']';
}
else if (data.isObject(obj))
{
logifiedMessage = `{${(typeof obj)}}\n\n${vt.code}${objName}:\n` + mcode.logify(mcode.logifyObject(obj));
}
else if (data.isJson(obj))
{
logifiedMessage = `{json}\n\n${vt.code}${objName}:\n` + mcode.logify(mcode.logifyObject(obj));
}
else
{
logifiedMessage = `{${(typeof obj)}}\n\n${vt.code}${objName}: ` + obj;
}
// flatten the exception object to strings for logging...
if (data.isObject(exception))
{
isExpObject = true;
if (exception.stack)
{
// colorized the passed the stack trace...
logifiedException = mcode.colorizeLines(exception.stack, vt.gray);
}
else
{
logifiedException = `${vt.reset}` + mcode.colorizeLines(mcode.logify(mcode.logifyObject(exception)), vt.code);
}
}
else if (data.isJson(exception))
{
logifiedException = mcode.colorizeLines(mcode.logifyObject(exception), vt.code);
}
else
{
logifiedException = mcode.colorizeLines(exception, vt.gray);
}
const [appModule, moduleLine] = this.getFrom(source);
let sevColor = vt.reset;
sevColor += vt.dead;
// created a simplified exception message for the log entry...
const loggedException = 'exception: ' + mcode.simplify(logifiedException);
// if 'loggedException' contains a stack trace, log it as an 'exception w/stack'
if (loggedException.includes('Error:') && loggedException.includes('at '))
{
isExpObject = true;
source = `${sevColor}exception${vt.reset}`;
}
if (isExpObject)
{
logText.push(`${vt.reset}${vt.dim}++\n`);
logText.push(`${vt.reset}${vt.dim} * 「mcode」: ${sevColor}💀 [${appModule}] '${logifiedMessage}'\n`);
logText.push(`${vt.reset}${vt.dim}${sevColor}exception:\n`);
logText.push(`${vt.reset}` + logifiedException + `\n`);
logText.push(`${vt.reset}${vt.dim} time: ${vt.reset}${mcode.timeStamp()}`);
logText.push(`${vt.reset}${vt.dim} from: ${vt.reset}${moduleLine}`);
logText.push(`${vt.reset}${vt.dim} severity: ${sevColor}exception w/stack${vt.reset}\n`);
logText.push(`${vt.reset}${vt.dim}--${vt.reset}`);
console.log(logText.join(''));
return `Object: ${objName} ${exception}`; // for caller to return
}
else
{
logText.push(`${vt.reset}${vt.dim}++\n`);
logText.push(`${vt.reset}${vt.dim} * 「mcode」: ${sevColor}💀 [${appModule}] '${logifiedMessage}'\n`);
logText.push(`${vt.reset}${vt.dim}${sevColor}${loggedException}${vt.gray}\n`);
logText.push(mcode.colorizeLines(`call stack: ${new Error().stack}\n`, vt.gray));
logText.push(`${vt.reset}${vt.dim} time: ${vt.reset}${mcode.timeStamp()}`);
logText.push(`${vt.reset}${vt.dim} from: ${vt.reset}${moduleLine}`);
logText.push(`${vt.reset}${vt.dim} severity: ${sevColor}exception w/trace${vt.reset}\n`);
logText.push(`${vt.reset}${vt.dim}--${vt.reset}`);
console.log(logText.join(''));
return `Object: ${objName} ${exception}`; // for caller to return
}
},
/**
* @func resx
* @memberof mcode
* @desc 'res' extension - logs an http response and returns the response result.
* @api public
* @param {object} res the response object.
* @param {string} action the action that was being performed.
* @param {object} response the response: {status, message, data, error}.
* @param {string} source where the message orginated.
* @returns {object} the response object.
*/
resx: function (res = {}, action = 'none', response = {}, source = '<unknown>.js>')
{
// example DB Entity: READ [200] OK, Entity: 'user' _id: nnnn-nnnn-nnnn-nnnn or Array: (n)
// example HTML Result: READ [200] OK, Endpoint: 'account.settings'
const id = response.id || response.data?.id || '';
const entity = response.entity;
const endpoint = response.endpoint || `<unknown>`;
const status = response.status || 0;
const countId = data.isArray(response.data) ? `Array: (${response.data.length})` : (id != '') ? `id: '${id}'` : ``;
const caller = (entity) ? `Entity: '${entity}' ${countId}` : `Endpoint: '${endpoint}'`;
const message = `${action.toUpperCase()} ${data.httpStatus(status)}, ${caller}`;
if (response.error)
{
// returning an error in the response...
this.exp(message, source, response.error);
return res.status(response.status).send({message: message, error: response.error});
}
if (response.data)
{
if (entity)
{
// returning Entity data in the response...
this.log(message, source, 'info');
return res.status(response.status).send({message: message, data: response.data});
}
else
{
// returning a direct Endpoint *result*, like HTML/HTMX - {0015}
this.log(message, source, 'info');
return res.status(response.status).send(response.data);
}
}
// log the response... (NOTE: debug messages are not logged in production mode - {0008})
this.log(message, source, 'debug');
// handle 'No Content' (204) response - {0017}
if (response.status === 204)
{
return res.status(204).end(); // to end request without a body and prevent client retry
}
// all other responses...
return res.status(response.status).send({message: message});
},
/**
* @func trace
* @memberof mcode
* @desc logs 'function call' showing call patterns to the Console in a standardized format.
* @api public
* @param {object} message pre-formatted message to be logged.
* @param {string} source where the message orginated.
* @returns nothing.
*/
trace: function (message = '<no message>', source = '<unknown>.js')
{
let vt = mcode.vt;
let logText = []; // build the response as an array for speed
let logifiedMessage = '';
// flatten the message object to strings for logging...
if (data.isObject(message))
{
logifiedMessage = '\n' + mcode.logify(mcode.logifyObject(message));
}
else if (data.isJson(message))
{
logifiedMessage = '\n' + mcode.logify(mcode.logifyObject(message));
}
else
{
logifiedMessage = message;
}
const [appModule, moduleLine] = this.getFrom(source);
let sevColor = vt.reset + vt.code;
// Function calls are always logged as 'Info'
logText.push(`${vt.reset}${vt.dim}++\n`);
logText.push(`${vt.reset}${vt.dim} µ 「mcode」: ${sevColor}🔍 [${appModule}] '${logifiedMessage}'${vt.reset}${vt.gray}\n`);
logText.push(mcode.colorizeLines(`call stack: ${new Error().stack}\n`, vt.gray));
logText.push(`${vt.reset}${vt.dim} time: ${vt.reset}${mcode.timeStamp()}`);
logText.push(`${vt.reset}${vt.dim} from: ${vt.reset}${moduleLine}`);
logText.push(`${vt.reset}${vt.dim} severity: ${sevColor}trace${vt.reset}\n`);
logText.push(`${vt.reset}${vt.dim}--${vt.reset}`);
console.log(logText.join(''));
},
/**
* @func simplify
* @memberof mcode
* @desc Strips a string of BRACES, BRACKETS, QUOTES, etc.
* @api public
* @param {object} object the string to be simplified to data
* @returns {string} the simplified text
*/
simplify: function (object)
{
if (data.isUndefined(object))
{
return 'undefined';
}
// flatten the message object to strings for logging...
if (data.isObject(object))
{
// do not use JSON.stringify(object, null, 4)
// --it's output is horrible, produce our own here in 'simplify()'
object = JSON.stringify(object);
}
let simplifiedText = '';
let inValue = false;
let inEscape = false;
let c = ' ';
let clast = ' ';
for (let i = 0; i < object.length; i++)
{
clast = c;
c = object[i];
// detect VT52,100,200 escape sequence
if (c === '\x1b')
{
inEscape = true;
continue;
}
// skip entire escape sequence
if (inEscape)
{
if (((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')))
{
inEscape = false;
}
continue;
}
switch (c)
{
case '{':
case '}':
case '[':
case ']':
inValue = false;
c = ' ';
break;
case '"':
c = ' ';
break;
case ':':
simplifiedText += c;
if (!inValue)
{
simplifiedText += ' ';
c = ' ';
}
inValue = true;
break;
case ',':
simplifiedText += c;
simplifiedText += ' ';
c = ' ';
inValue = false;
break;
case '\n':
case '\t':
c = ' ';
break; // strip newlines and tabs
case ' ':
if (clast != ' ')
{
simplifiedText += c;
}
break;
default:
simplifiedText += c;
break;
}
}
return simplifiedText;
},
/**
* @func simplifyObject
* @memberof mcode
* @desc Strips an object of BRACES, BRACKETS, QUOTES, etc.
* @api public
* @param {object} objectToSimplify the object to be formatted for the event log
* @returns {string} the simplified object
*/
simplifyObject: function (objectToSimplify)
{
// do not use JSON.stringify(object, null, 4) -- it's output is horrible, use our own
return mcode.simplify(JSON.stringify(objectToSimplify));
},
/**
* @func logify
* @memberof mcode
* @desc Formats a string of BRACES, BRACKETS, QUOTES, for display in the EVENT LOG.
* No formatting occurs until the opening brace '{' of the JSON Data. VT Escape sequences are stripped.
* @api public
* @param {string} textToLogify the string to be formatted for the event log
* @returns {string} the logified text
*/
logify: function (textToLogify)
{
let vt = mcode.vt;
let inJson = false; // start formatting when we hit the first '{'
let inValue = false; // handle 'true, false, null, or number' as-is
let inString = false; // handle "quoted strings" as-is
let inLiteral = false; // take internal text as-is
let logText = []; // build the response as an array for speed
let tabStop = 0; // indent level for formatting
let lineEmpty = true; // controls indent() output
// ƒ to remove surrounding " " from key names only.
let keyPairs = (jsonString) =>
{
// loop backward through the string, building a new copy, remove " " from key names
let newString = '';
let inKey = false;
let inKeyName = false;
let inString = false;
let c = '';
for (let i = jsonString.length - 1; i >= 0; i--)
{
c = jsonString[i];
if (inString)
{
if (c === '"')
{
inString = false;
newString = c + newString;
continue;
}
newString = c + newString;
continue;
}
if (inKeyName)
{
if (c === '"')
{
inKeyName = false;
inKey = false;
continue;
}
else
{
newString = c + newString;
continue;
}
}
if (inKey)
{
if (c === '"')
{
inKeyName = true;
continue;
}
else
{
newString = c + newString;
continue;
}
}
if (c === '"')
{
newString = c + newString;
inString = true;
continue;
}
if (c === ':')
{
inKey = true;
}
newString = c + newString;
}
return newString;
};
// ƒ to indent the JSON
let indent = () =>
{
let newline = '';
if (!lineEmpty)
{
newline += '\n' + `${vt.reset}`;
for (let index = 0; index < tabStop; index++)
{
newline += ' '; // 4-space tab-stop
}
lineEmpty = true;
}
return newline;
};
// ƒ to check for legal value name characters
let isKeyChar = (c) =>
{
const code = c.charCodeAt(0);
return (c !== '"') && (c !== ':') && (code >= 32 && code <= 126);
};
// ƒ to check for alpha-numeric characters
let isValueChar = (c) =>
{
return (c === '-') || (c === '_') || (c === ' ') || (c === '$') || (c === '.') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
};
// S T A R T P R O C E S S I N G T H E J S O N S T R I N G
textToLogify = keyPairs(textToLogify);
let cc = ''; // 'cc' - current character, vs. 'c' - temporary character
for (let i = 0; i < textToLogify.length; i++)
{
cc = textToLogify[i];
if (textToLogify.substring(i, i + 2) === '\\\\')
{
// take backslash as-is
logText.push(cc);
logText.push(cc);
i++; // skip the next '\'
continue;
}
if (!inString && textToLogify.substring(i, i + 2) === '\\n')
{
logText.push(indent());
lineEmpty = false;
i++; // skip the 'n'
continue;
}
if (inLiteral)
{
logText.push(cc);
if (cc === '}')
{
inLiteral = false;
}
continue;
}
if (textToLogify.substring(i, i + 2) === '${')
{
inLiteral = true;
logText.push(cc);
continue;
}
if (!inString && !inJson && cc === '{')
{
inJson = true;
--i; // reprocess '{' as JSON
continue;
}
if (inValue)
{
if (!isValueChar(cc))
{
inValue = false;
--i; // reprocess non-alpha-numeric character outside of 'value'
}
else
{
logText.push(cc);
}
continue;
}
if (inString)
{
if (cc === '"')
{
inString = false;
cc = '\"' + `${mcode.vt.reset}`;
}
logText.push(cc);
continue;
}
if (!inJson)
{
logText.push(cc);
lineEmpty = false;
continue;
}
switch (cc)
{
case '{':
logText.push(indent() + '{');
lineEmpty = false;
tabStop++;
logText.push(indent());
break;
case '[':
logText.push(indent() + '[');
lineEmpty = false;
tabStop++;
logText.push(indent());
break;
case '}':
tabStop--;
logText.push(