@node-in-layers/core
Version:
The core library for the Node In Layers rapid web development framework.
357 lines • 13.4 kB
JavaScript
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