UNPKG

simple-log-methods

Version:

a simple and opinionated logging library. plays well with aws lambda + cloudwatch.

146 lines 7.61 kB
"use strict"; 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