All files LoggerWrapper.js

100% Statements 26/26
100% Branches 18/18
100% Functions 5/5
100% Lines 26/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118                              14x                                     12x     1x         11x 1x       11x 11x       11x 1x   10x 10x       11x 11x   11x 11x 11x   11x   2x     11x                     1x                     3x   3x                       2x   2x   2x       1x  
'use strict';
 
/**
 * This is a wrapper for the logger provided by service runner.
 * It provides more user-friendly logging APIs and better error
 * signaling for when it is used incorrectly.
 *
 * This is the logger that other scripts in this project will interact with.
 * Usage:
 * const logger = new LoggerWrapper( <somelogger> );
 * logger.log('warn', 'hello this is a message');
 *
 */
class LoggerWrapper {
	constructor( logger ) {
		this._logger = logger;
	}
 
	/**
	 * Logs a message on a given severity level.
	 * Acceptable levels: 'trace', 'debug', 'info', 'warn', 'error', and 'fatal'.
	 *
	 * @param {string} level Severity level using one of the level options.
	 * Can also be: 'trace' or 'trace/request'.
	 *
	 * @param {Object} data Contains message and any relevant info for the log.
	 * @param {string|undefined} data.message Log message string
	 * @param {string|undefined} data.requestId The 'x-request-id' HTTP header, for traceability
	 * @param {string|Object|undefined} data.info request details or JSON object
	 */
	log( level, data = { message: 'No message set!', requestId: 'No requestId set!', info: 'No info set!' } ) {
		// TODO (T369560):
		// confirm in Logstash Prod that message param is key of an object, not string;
		// and that is what actually gets emitted as 'message' && 'msg'; i.e. function-evaluator
		if ( !level || !data ) {
			// The service runner implementation will just silently no-op
			// in this situation. We want to alert the caller here.
			throw new Error(
				`Incorrect usage of the logger. Both arguments need to be
				present. E.g. logger.log(level, data).` );
		}
		// temporarily adding this in case there's an undetected old version of logging somewhere
		if ( typeof data === 'string' ) {
			data = { message: data };
		}
 
		// We want to output the request ID under this special name, but it's awkward.
		data[ 'x-request-id' ] = data.requestId;
		delete data.requestId;
 
		// add or verify timestamp per log
		let timeStamp;
		if ( data.time && this._isValidTimeStamp( data.time ) ) {
			timeStamp = data.time;
		} else {
			timeStamp = new Date().toISOString();
			data.time = timeStamp;
		}
 
		// add stacktrace per log
		const stackTrace = new Error().stack;
		const detailedStack = stackTrace.split( '\n' ).slice( 2 ).join( '\n' );
		// adding compacted details to message, in case data object gets swallowed up in Logstash
		const simpleTrace = detailedStack.split( '\n' )[ 0 ].trim();
		data.trace = simpleTrace;
		data.message = data.message + `, time: ${ timeStamp }, reqId: ${ data[ 'x-request-id' ] }, trace: ${ simpleTrace }`;
		// this is so we can easily debug/detect in docker logs
		if ( process.env.WIKIFUNCTIONS_DEBUG_LOCAL ) {
			// eslint-disable-next-line no-console
			console.log( 'Logging LEVEL:', level, ', MESSAGE:', data.message, ', DATA:', data, ', DETAILS:', detailedStack );
		}
 
		this._logger.log( level, data );
	}
 
	/**
	 * Creates a child logger for a sub-component of your application.
	 * This directly wraps its core logger obj's implementation.
	 *
	 * @param {*} args arguments for the child wrapper.
	 * @return {LoggerWrapper} A new logger for the sub-component.
	 */
	child( args ) {
		return new LoggerWrapper( this._logger.child( args ) );
	}
 
	/**
	 * Validates given timestamp.
	 * (Expected to come from the Executor which does not have logger access)
	 *
	 * @param {string} currentTimeStamp an ISO date string OR nothing OR something invalid...
	 * @return {boolean} true || false
	 */
	_isValidTimeStamp( currentTimeStamp ) {
		const dateFromIsoDateString = new Date( currentTimeStamp );
		// Check if the date is valid and matches the original input
		return !isNaN( dateFromIsoDateString.getTime() ) &&
			dateFromIsoDateString.toISOString() === currentTimeStamp &&
			this._isCurrentDate( dateFromIsoDateString );
	}
 
	/**
	 * Checks if the date is current and valid
	 *
	 * @param {Object} dateInput date object, i.e. Tue Jun 25 2024 12:34:56 GMT+0000
	 * @return {boolean} true || false
	 */
	_isCurrentDate( dateInput ) {
		const dateToday = new Date();
		const pastLimit =
			new Date( dateToday.getFullYear() - 100, dateToday.getMonth(), dateToday.getDate() );
 
		return dateInput <= dateToday && dateInput >= pastLimit;
	}
}
 
module.exports = { LoggerWrapper };