simple-log-methods
Version:
a simple and opinionated logging library. plays well with aws lambda + cloudwatch.
146 lines • 7.61 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.withLogTrail = void 0;
const uni_time_1 = require("@ehmpathy/uni-time");
const helpful_errors_1 = require("helpful-errors");
const type_fns_1 = require("type-fns");
const noOp = (...input) => input;
const omitContext = (...input) => input[0]; // standard pattern for args = [input, context]
const pickErrorMessage = (input) => ({
error: { message: input.message },
});
const roundToHundredths = (num) => Math.round(num * 100) / 100; // https://stackoverflow.com/a/14968691/3068233
const DEFAULT_DURATION_REPORT_THRESHOLD = process.env
.VISUALOGIC_DURATION_THRESHOLD
? parseInt(process.env.VISUALOGIC_DURATION_THRESHOLD)
: (0, uni_time_1.toMilliseconds)({ seconds: 1 });
/**
* enables input output logging and tracing for a method
*
* todo: - add tracing identifier w/ async-context
* todo: - hookup visual tracing w/ external lib (vi...lo...)
* todo: - bundle this with its own logging library which supports scoped logs
*/
const withLogTrail = (logic, { name: declaredName, log: logOptions, duration = {
threshold: { milliseconds: DEFAULT_DURATION_REPORT_THRESHOLD },
}, }) => {
// cache the name of the function per wrapping
const name = logic.name || declaredName || null; // use `\\` since `logic.name` returns `""` for anonymous functions
// if no name is identifiable, throw an error here to fail fast
if (!name)
throw new helpful_errors_1.UnexpectedCodePathError('could not identify name for wrapped function');
// if the name specified does not match the name of the function, throw an error here to fail fast
if (declaredName && name !== declaredName)
throw new helpful_errors_1.UnexpectedCodePathError('the natural name of the function is different than the declared name', { declaredName, naturalName: name });
// extract the log levels per operation
const logLevelInput = (typeof logOptions?.level === 'object'
? logOptions.level.input
: logOptions?.level) ?? 'debug';
const logLevelOutput = (typeof logOptions?.level === 'object'
? logOptions.level.output
: logOptions?.level) ?? 'debug';
const logLevelError =
// note: error level is only overridable via the object form, to prevent `level: 'info'` from accidentally downgrading error logs
(typeof logOptions?.level === 'object' ? logOptions.level.error : null) ??
'warn';
// extract the log methods
const logInputMethod = logOptions?.input ?? omitContext;
const logOutputMethod = logOptions?.output ?? noOp;
const logErrorMethod = logOptions?.error ?? pickErrorMessage;
// define the duration threshold
const durationReportingThreshold = duration.threshold;
const durationReportingThresholdInSeconds = (0, uni_time_1.toMilliseconds)(durationReportingThreshold) / 1000;
// wrap the function
return (input, context) => {
// now log the input
context.log[logLevelInput](`${name}.input`, {
input: logInputMethod(input, context),
});
// begin tracking duration
const startTimeInMilliseconds = new Date().getTime();
// define the context.log method that will be given to the logic
const logMethodsWithContext = {
// add the trail
trail: [...(context.log.trail ?? []), name],
// track the orig logger
_orig: context.log?._orig ?? context.log,
// add the scoped methods
debug: (message, metadata) => context.log.debug(`${name}.progress: ${message}`, metadata),
info: (message, metadata) => context.log.info(`${name}.progress: ${message}`, metadata),
warn: (message, metadata) => context.log.warn(`${name}.progress: ${message}`, metadata),
error: (message, metadata) => context.log.error(`${name}.progress: ${message}`, metadata),
};
// define what to do when we have an error
const logError = (error) => {
const endTimeInMilliseconds = new Date().getTime();
const durationInMilliseconds = endTimeInMilliseconds - startTimeInMilliseconds;
const durationInSeconds = roundToHundredths(durationInMilliseconds / 1e3); // https://stackoverflow.com/a/53970656/3068233
context.log[logLevelError](`${name}.error`, {
input: logInputMethod(input, context),
output: logErrorMethod(error),
...(durationInSeconds >= durationReportingThresholdInSeconds
? { duration: `${durationInSeconds} sec` } // only include the duration if the threshold was crossed
: {}),
});
};
// now execute the method, wrapped to catch sync errors
let result;
try {
result = logic(input, {
...context,
log: logMethodsWithContext,
});
}
catch (error) {
// log the error for sync functions that throw
if (error instanceof Error)
logError(error);
throw error;
}
// if the result was a promise, log when that method crosses the reporting threshold, to identify which procedures are slow
if ((0, type_fns_1.isAPromise)(result)) {
// define how to log the breach, on breach
const onDurationBreach = () => context.log[logLevelOutput](`${name}.duration.breach`, {
input: logInputMethod(input, context),
already: { duration: `${durationReportingThresholdInSeconds} sec` },
});
// define a timeout which will trigger on duration threshold
const onBreachTrigger = setTimeout(onDurationBreach, durationReportingThresholdInSeconds * 1000);
// remove the timeout when the operation completes, to prevent logging if completes before duration
void result
.finally(() => clearTimeout(onBreachTrigger))
.catch(() => {
// do nothing when there's an error; just catch it, to ensure it doesn't get propagated further as an uncaught exception
});
}
// define what to do when we have output
const logOutput = (output) => {
const endTimeInMilliseconds = new Date().getTime();
const durationInMilliseconds = endTimeInMilliseconds - startTimeInMilliseconds;
const durationInSeconds = roundToHundredths(durationInMilliseconds / 1e3); // https://stackoverflow.com/a/53970656/3068233
context.log[logLevelOutput](`${name}.output`, {
input: logInputMethod(input, context),
output: logOutputMethod(output),
...(durationInSeconds >= durationReportingThresholdInSeconds
? { duration: `${durationInSeconds} sec` } // only include the duration if the threshold was crossed
: {}),
});
};
// if result is a promise, ensure we log after the output resolves
if ((0, type_fns_1.isAPromise)(result))
return result
.then((output) => {
logOutput(output);
return output;
})
.catch((error) => {
logError(error);
throw error;
});
// otherwise, its not a promise, so its done, so log now and return the result
logOutput(result);
return result;
};
};
exports.withLogTrail = withLogTrail;
//# sourceMappingURL=withLogTrail.js.map