src/decorators/logMethod.decorator.ts
import type { CommonLogger } from '..'
import { SimpleMovingAverage, _stringify, _assert } from '..'
import { _ms } from '../time/time.util'
import { _getArgsSignature, _getMethodSignature } from './decorator.util'
/**
* $r - result
*
* @returns array of tokens that will be `.filter(Boolean).join(' ')`
*/
type LogResultFn = (r: any) => any[]
export interface LogMethodOptions {
/**
* Log "moving average" elapsed time for up to `avg` last method calls
*/
avg?: number
/**
* Defaults to true.
* Set to false to skip logging method arguments
*/
logArgs?: boolean
/**
* Defaults to true.
* Set to false to skip logging result length when result is an array.
*/
logResultLength?: boolean
/**
* Also log on method start.
* Example:
*
* >> syncMethodSuccess()
*/
logStart?: boolean
/**
* Log method result as is (stringified).
* Example:
*
* * << syncMethodSuccess() took 124 ms result: 'SomeString as result'
*/
logResult?: boolean
/**
* Log method result via provided function that takes "result as is" as first argument and should return a String.
* Overrides `logResult`.
*/
logResultFn?: LogResultFn
/**
* Defaults to `console`
*/
logger?: CommonLogger
}
/**
* Console-logs when method had started, when it finished, time taken and if error happened.
* Supports both sync and async methods.
* Awaits if method returns a Promise.
*
* @example output:
*
* >> syncMethodSuccess()
* << syncMethodSuccess() took 124 ms
*
* >> asyncMethod()
* << asyncMethodThrow() took 10 ms ERROR: MyError
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export function _LogMethod(opt: LogMethodOptions = {}): MethodDecorator {
return (target, key, descriptor) => {
_assert(typeof descriptor.value === 'function', '@_LogMethod can be applied only to methods')
const originalFn = descriptor.value
const keyStr = String(key)
const {
avg,
logArgs = true,
logStart,
logResult,
logResultLength = true,
logger = console,
} = opt
let { logResultFn } = opt
if (!logResultFn) {
if (logResult) {
logResultFn = r => ['result:', _stringify(r)]
} else if (logResultLength) {
logResultFn = r => (Array.isArray(r) ? [`result: ${r.length} items`] : [])
}
}
const sma = avg ? new SimpleMovingAverage(avg) : undefined
let count = 0
descriptor.value = function (this: typeof target, ...args: any[]) {
const started = Date.now()
const ctx = this
// e.g `NameOfYourClass.methodName`
// or `NameOfYourClass(instanceId).methodName`
const methodSignature = _getMethodSignature(ctx, keyStr)
const argsStr = _getArgsSignature(args, logArgs)
const callSignature = `${methodSignature}(${argsStr}) #${++count}`
if (logStart) logger.log(`>> ${callSignature}`)
try {
const res = originalFn.apply(ctx, args)
if (res && typeof res.then === 'function') {
// Result is a Promise, will wait for resolution or rejection
return res
.then((r: any) => {
logFinished(logger, callSignature, started, sma, logResultFn, r)
return r
})
.catch((err: any) => {
logFinished(logger, callSignature, started, sma, logResultFn, undefined, err)
throw err
})
}
// not a Promise
logFinished(logger, callSignature, started, sma, logResultFn, res)
return res
} catch (err) {
logFinished(logger, callSignature, started, sma, logResultFn, undefined, err)
throw err // rethrow
}
} as any
return descriptor
}
}
// eslint-disable-next-line max-params
function logFinished(
logger: CommonLogger,
callSignature: string,
started: number,
sma?: SimpleMovingAverage,
logResultFn?: LogResultFn,
res?: any,
err?: any,
): void {
const millis = Date.now() - started
const t = ['<<', callSignature, 'took', _ms(millis)]
if (sma) {
t.push(`(avg ${_ms(sma.pushGetAvg(millis))})`)
}
if (err !== undefined) {
t.push('ERROR:', err)
} else if (logResultFn) {
t.push(...logResultFn(res))
}
logger.log(...t.filter(Boolean))
}