UNPKG

@node-in-layers/core

Version:

The core library for the Node In Layers rapid web development framework.

357 lines 13.4 kB
import merge from 'lodash/merge.js'; import flatten from 'lodash/flatten.js'; import get from 'lodash/get.js'; import omit from 'lodash/omit.js'; import safeJson from 'safe-json-value'; import axios from 'axios'; import { v4 } from 'uuid'; import { createErrorObject, getLogLevelNumber, isErrorObject } from '../libs.js'; import { CoreNamespace, LogFormat, LogLevel, LogLevelNames, } from '../types.js'; import { memoizeValueSync } from '../utils.js'; import { defaultGetFunctionWrapLogLevel, combineLoggingProps, capForLogging, extractCrossLayerProps, } from './lib.js'; const MAX_LOGGING_ATTEMPTS = 5; const _isPromise = (obj) => { return obj && obj.then; }; const _combineIds = (id) => { return id .map(i => { return Object.entries(i) .map(([key, value]) => { return `${key}:${value}`; }) .join(';'); }) .join(';'); }; /** * A simple LogFunction that provides datetime: message * @param logMessage */ const consoleLogSimple = (logMessage) => { // @ts-ignore // eslint-disable-next-line no-console console[logMessage.logLevel](`${logMessage.datetime.toISOString()}: ${logMessage.message}`); }; /** * A simple LogFunction that logs more data as a single string. * @param logMessage */ const consoleLogFull = (logMessage) => { return logMessage.ids ? // @ts-ignore // eslint-disable-next-line no-console console[logMessage.logLevel](`${logMessage.datetime.toISOString()} ${logMessage.environment} ${logMessage.logLevel} ${logMessage.id} [${logMessage.logger}] {${_combineIds(logMessage.ids)}} ${logMessage.message}`) : // eslint-disable-next-line no-console console[logMessage.logLevel](`${logMessage.datetime.toISOString()} ${logMessage.environment} ${logMessage.logLevel} [${logMessage.logger}] ${logMessage.message}`); }; /** * A simple LogFunction that logs a message as a json object. * @param logMessage */ const consoleLogJson = (logMessage) => { // @ts-ignore // eslint-disable-next-line no-console console[logMessage.logLevel](JSON.stringify({ id: logMessage.id, datetime: logMessage.datetime.toISOString(), logLevel: logMessage.logLevel, logger: logMessage.logger, message: logMessage.message, // Remove the ones listed above ...omit(logMessage, [ 'id', 'datetime', 'message', 'logger', 'logLevel', ]), }, null)); }; const _shouldIgnore = (logLevel, messageLogLevel) => { const asInt = getLogLevelNumber(logLevel); if (asInt === LogLevel.SILENT) { return true; } const asInt2 = getLogLevelNumber(messageLogLevel); return asInt > asInt2; }; /** * A LogMethod that creates a TCP call using a LogMessage object as the body. * tcpLoggingOptions must be set in the configuration file. * @param context */ const logTcp = (context) => { const tcpOptions = context.config[CoreNamespace.root].logging.tcpLoggingOptions; if (!tcpOptions) { throw new Error(`Must include tcpLoggingOptions when using a tcp logger`); } const url = tcpOptions.url; const axiosInstance = axios.create({ baseURL: url, headers: tcpOptions.headers, }); return (logMessage) => { // Sometimes, logging frameworks can fail, so we should try multiple attempts. return [...new Array(MAX_LOGGING_ATTEMPTS)].reduce(async (accP) => { const acc = await accP; if (acc) { return acc; } return axiosInstance({ data: logMessage, }) .then(() => { return true; }) .catch(e => { // TODO: Narrow down the scope of these catches console.warn('Logging error'); console.warn(e); return false; }); }, Promise.resolve(undefined)); }; }; const _getLogMethodFromFormat = (logFormat) => { if (Array.isArray(logFormat)) { return flatten(logFormat.map(_getLogMethodFromFormat)); } switch (logFormat) { case LogFormat.custom: throw new Error(`This should never be here. customLogger should override this`); case LogFormat.json: return [() => consoleLogJson]; case LogFormat.simple: return [() => consoleLogSimple]; case LogFormat.full: return [() => consoleLogFull]; case LogFormat.tcp: return [logTcp]; default: throw new Error(`LogFormat ${logFormat} is not supported`); } }; const _layerLogger = (context, subLogger, layerName, crossLayerProps) => { const theLogger1 = subLogger.getSubLogger(layerName).applyData({ layer: layerName, }); // we have to do this, to get the id being created const theLogger = theLogger1.applyData(combineLoggingProps(theLogger1, crossLayerProps)); const logLevelGetter = get(context, `config${CoreNamespace.root}.logging.getFunctionWrapLogLevel`) || defaultGetFunctionWrapLogLevel; const getFunctionLogger = (functionName, crossLayerProps) => { const funcLogger = theLogger .getIdLogger(functionName, 'functionCallId', v4()) .applyData({ function: functionName, }); return funcLogger.applyData(combineLoggingProps(funcLogger, crossLayerProps)); }; const getInnerLogger = (functionName, crossLayerProps) => { const funcLogger = theLogger.getSubLogger(functionName).applyData({ function: functionName, }); return funcLogger.applyData(combineLoggingProps(funcLogger, crossLayerProps)); }; const logWrap = (functionName, func) => { // @ts-ignore const logLevel = logLevelGetter(layerName, functionName); return (...a) => { const [argsNoCrossLayer, crossLayer] = extractCrossLayerProps(a); const funcLogger = getFunctionLogger(functionName, crossLayer); funcLogger[logLevel](`Executing ${layerName} function`, { args: argsNoCrossLayer, }); // eslint-disable-next-line functional/no-try-statements try { // @ts-ignore const result = func(funcLogger, ...argsNoCrossLayer, { logging: { ids: funcLogger.getIds(), }, }); // If a promise. if (_isPromise(result)) { return result .then(r => { funcLogger[logLevel](`Executed ${layerName} function`, { result: r, }); return r; }) .catch(e => { funcLogger.error('Function failed with an exception', createErrorObject('INTERNAL_ERROR', `Layer function ${layerName}:${functionName}`, e)); throw e; }); } // If not a promise funcLogger[logLevel](`Executed ${layerName} function`, { result, }); return result; } catch (e) { funcLogger.error('Function failed with an exception', createErrorObject('INTERNAL_ERROR', `Layer function ${layerName}:${functionName}`, e)); throw e; } }; }; // @ts-ignore return merge({}, theLogger, { getInnerLogger, getFunctionLogger, _logWrap: logWrap, // eslint-disable-next-line functional/prefer-tacit _logWrapAsync: (functionName, func) => { return logWrap(functionName, func); }, // eslint-disable-next-line functional/prefer-tacit _logWrapSync: (functionName, func) => { return logWrap(functionName, func); }, }); }; const _appLogger = (context, subLogger, appName) => { const theLogger = subLogger.getSubLogger(appName).applyData({ app: appName, }); return merge({}, theLogger, { getLayerLogger: (layerName, crossLayerProps) => { return _layerLogger(context, theLogger, layerName, crossLayerProps); }, }); }; /** * Creates a sub logger * @param context - The context * @param logMethods - The logging methods * @param props - Values that are passed along to each sub logger. */ const _subLogger = (context, logMethods, props) => { const theLogLevel = context.config[CoreNamespace.root].logging.logLevel; const getLogMethods = logMethods.map(memoizeValueSync); const _doLog = (logLevel) => (message, dataOrError, options) => { if (_shouldIgnore(theLogLevel, logLevel)) { return undefined; } const funcs = getLogMethods.map(x => x(context)); const isError = isErrorObject(dataOrError); const { value: data } = safeJson(merge({}, props.data, isError ? {} : dataOrError)); const theData = options?.ignoreSizeLimit ? data || {} : capForLogging(data, context.config[CoreNamespace.root].logging.maxLogSizeInCharacters); const logMessage = { id: v4(), environment: context.constants.environment, datetime: new Date(), logLevel, message, ids: props.ids, logger: props.names.join(':'), ...(isError ? { error: dataOrError.error } : {}), ...theData, ...omit(props, ['ids', 'names', 'data', 'error']), }; const result = funcs.map(x => { return x(logMessage); }); const promises = result.filter(_isPromise); if (promises.length > 0) { return Promise.resolve().then(async () => { await Promise.all(promises); return; }); } return undefined; }; return { getIds: () => get(props, 'ids', []), debug: _doLog(LogLevelNames.debug), info: _doLog(LogLevelNames.info), warn: _doLog(LogLevelNames.warn), trace: _doLog(LogLevelNames.debug), error: _doLog(LogLevelNames.error), getSubLogger: (name) => { return _subLogger(context, logMethods, { names: props.names.concat(name), ids: props.ids, data: props.data, }); }, getIdLogger: (name, logIdOrKey, value) => { const isObject = typeof logIdOrKey === 'object'; if (!isObject) { if (!value) { throw new Error(`Need value if providing a key`); } } const logId = isObject ? logIdOrKey : { [logIdOrKey]: value }; return _subLogger(context, logMethods, { names: props.names.concat(name), ids: get(props, 'ids', []).concat(logId), data: props.data, }); }, applyData: (data) => { const merged = Object.assign({}, props, data, { ids: data?.ids ? data.ids : props.ids, }); return _subLogger(context, logMethods, merged); }, }; }; const _getIdsWithRuntime = (runtimeId, props) => { const theIds = [{ runtimeId }]; if (!props?.ids) { return theIds; } const ids = props.ids; const hasRuntimeId = ids.find(obj => { return Object.keys(obj).find(y => y === 'runtimeId'); }); if (hasRuntimeId) { return props.ids; } return theIds.concat(props.ids); }; /** * The standard RootLogger for the core library. */ const standardLogger = () => { const getLogger = (context, props) => { if (context.config[CoreNamespace.root].logging.customLogger) { const ids = _getIdsWithRuntime(context.constants.runtimeId, props); return context.config[CoreNamespace.root].logging.customLogger.getLogger(context, merge({}, props, { ids })); } const logMethods = _getLogMethodFromFormat(context.config[CoreNamespace.root].logging.logFormat); return compositeLogger(logMethods).getLogger(context, props); }; return { getLogger, }; }; /** * A useful RootLogger that can combine multiple logging methods together. Useful as the best of custom RootLoggers * because this provides everything, except the actual function that does the logging. * @param logMethods - A list of log methods. */ const compositeLogger = (logMethods) => { const getLogger = (context, props) => { const ids = _getIdsWithRuntime(context.constants.runtimeId, props); const subLogger = _subLogger(context, logMethods, { names: [], ids, ...(props?.data ? props.data : {}), }); return merge(subLogger, { getAppLogger: (appName) => { return _appLogger(context, subLogger, appName); }, }); }; return { getLogger, }; }; export { standardLogger, consoleLogSimple, consoleLogJson, consoleLogFull, compositeLogger, logTcp, }; //# sourceMappingURL=logging.js.map