decorators/access-logger.js

var _fs      = require("fs");
var _path    = require("path");
var _cluster = require("cluster");
/**
 * @class
 * @name AccessLogger
 * @description Access Logger for HTTP Web Servers
 */
function AccessLogger() {
	this.months = [
		"January",
		"February",
		"March",
		"April",
		"May",
		"June",
		"July",
		"August",
		"September",
		"October",
		"November",
		"December"
	];
	/**
	 * @memberof AccessLogger#
	 * @name combined
	 * @type {boolean}
	 * @description log format as combined, with user agent: %h %e %^[%x] "%r" %s %b "%R" "{%u}"
	 */
	this.combined = false;	
	/**
	 * @memberof AccessLogger#
	 * @name logsIncludeHostname
	 * @type {boolean}
	 * @description log format for vhosts %v, ex: %h %e %^[%x] "{%v}" "%r" %s %b "%R"
	 */
	this.logsIncludeHostname = false;
};

/**
 * @memberof AccessLogger#
 * @description for go access: tail -c 67108864 -f '/var/logs/default/2024/2/data-master.log' | goaccess --log-format='%h %e %^[%x] "%v" "%r" %s %b "%R" "%u"' --date-format='%d/%b/%Y:%H:%M:%S %z' --time-format='%d/%b/%Y:%H:%M:%S %z' -
 * with combined and logger %h %e %^[%x] "%r" %s %b "%R"
 * @method logRequest
 * @param {IncomingMessage} request 
 * @param {ServerResponse} response 
 * @returns {string}
 */
AccessLogger.prototype.logRequest = function (request, response) {
	// %h %e %^[%x] "{%v}" "%r" %s %b "%R" "{%u}"
	return this.getRemoteIp(request) + ' - ' +
		this.getUsername(request) + ' [' +
		this.formattedDate(new Date()) + '] ' +
		(
			this.logsIncludeHostname ? (
				'"' + (request.headers.host + '').toLowerCase() + '" '
			) : '-'
		) + '"' +
		(request.method || '-') + ' ' +
		(request.url || '-') + ' ' +
		this.getProtocol(request) + '/' +
		request.httpVersion + '" ' +
		response.statusCode + ' ' +
		((response.socket || response.connection || {}).bytesWritten || '-') + ' "' +
		(this.getReferer(request) || "-" ) + '"' + (
			this.combined ? (
				' "' +
				(request.headers['user-agent'] || "-") + '"'
			) : ''
		);
};

/**
 * @memberof AccessLogger#
 * @method formattedDate
 * @param {Date} timeStamp 
 * @returns {string}
 */
AccessLogger.prototype.formattedDate = function formattedDate(timeStamp) {
	var _2d = function (i) {
		return ((i < 10) ? ('0' + i) : (
			i > 99 ? (
				i + ''
			) : ('' + i)
		));
	};
    return _2d(timeStamp.getUTCDate()) + '/' +
        this.months[timeStamp.getMonth()].substring(0, 3) + '/' +
        _2d(timeStamp.getUTCFullYear()) + ':' +
        _2d(timeStamp.getUTCHours()) + ':' +
        _2d(timeStamp.getUTCMinutes()) + ':' +
        _2d(timeStamp.getUTCSeconds()) + ' +0000';
};

/**
 * @memberof AccessLogger#
 * @method getUsername
 * @param {IncomingMessage|SGAppsServerRequest} request 
 * @returns {string}
 */
AccessLogger.prototype.getUsername = function getUsername(request) {
    var username = "-";
	//@ts-ignore
    if (request.session && request.session.user) {
		//@ts-ignore
        username = request.session.user ? request.session.user.username : '';
    }
	
	//@ts-ignore
	if (username !== '-' && request.session && (request.session.id || request.session._id)) {
		//@ts-ignore
        username = request.session._id || request.session.id;
    }
    return username;
};

/**
 * @memberof AccessLogger#
 * @method getReferer
 * @param {IncomingMessage|SGAppsServerRequest} request 
 * @returns {string}
 */
AccessLogger.prototype.getReferer = function (request) {
	//@ts-ignore
    return (request.request || request).headers.referer || (request.request || request).headers.referrer || null;
};

/**
 * @memberof AccessLogger#
 * @method getProtocol
 * @param {IncomingMessage|SGAppsServerRequest} request
 * @returns {string}
 */
AccessLogger.prototype.getProtocol = function (request) {
	/**
	 * @private
	 * @type {IncomingMessage}
	 */
	//@ts-ignore
	var message = (request.request || request);

    if (message.socket && message.socket.localPort === 443) {
        return "HTTPS";
    }
    return "HTTP";
};

/**
 * @memberof AccessLogger#
 * @method getSize
 * @param {Buffer|string} data
 * @returns {number}
 */
AccessLogger.prototype.getSize = function (data) {
	if (data === null) return 0;
    if (Buffer.isBuffer(data)) {
        return data.length;
    }
    if (typeof data === 'string') {
        return Buffer.byteLength(data);
    }
	return null;
};

/**
 * @memberof AccessLogger#
 * @method getRemoteIp
 * @param {IncomingMessage} request
 * @returns {string}
 */
AccessLogger.prototype.getRemoteIp = function (request) {
	var getData = function (data) {
		var result = data;
		if (Array.isArray(data)) result = data[0];
		
		if (result) {
			result += '';
			result = result.replace(/[\s\n]+/g, ',').replace(/\,+/g, ',')
		}
		
		return result;
	};

	return getData(request.headers['x-forwarded-for']) || getData(request.headers['X-Forwarded-For']) || getData(request.connection.remoteAddress) || "???.???.???.???";
};

/**
 * @memberof AccessLogger
 * @callback AccessLoggerHandle
 * @param {string} dataLog
 * @returns {null|string}
 */
/**
 * @memberof AccessLogger
 * @typedef {object} AccessLoggerPath
 * @property {boolean} [isEnabled=false]
 * @property {string|null} [path] file path where logs will be written, placeholders: {year} {month} {date} {day} {pid} {worker-id}
 * @property {boolean} [waitAllHandlers=false] file path where logs will be written
 * @property {AccessLogger.AccessLoggerHandle|null} [handle]
 */

/***
 * @private
 * @type {Object<string,import("fs").WriteStream>}
 */
var AccessLoggerWriteStreams = {};

var AccessLoggerWriter = function (path, data) {
	var err;
	var logPath = _path.resolve(path);
	if (logPath in AccessLoggerWriteStreams) {
		if (AccessLoggerWriteStreams[logPath] !== null) {
			if (AccessLoggerWriteStreams[logPath].writable) {
				AccessLoggerWriteStreams[logPath].write(data + '\n');
			}
		}
	} else {
		var date = new Date();
		var filePath = logPath.replace(
			'{pid}',
			process.pid + ''
		).replace(
			'{year}',
			date.getFullYear() + ''
		).replace(
			'{month}',
			(date.getMonth() + 1) + ''
		).replace(
			'{date}',
			date.getDate() + ''
		).replace(
			'{day}',
			date.getDay() + ''
		).replace(
			'{worker-id}',
			( _cluster.worker ? _cluster.worker.id : 'master' ) + ''
		);

		var dirPath = _path.dirname(filePath);

		var err, dataStream;
		try {
			_fs.mkdirSync(dirPath, { recursive: true });
			dataStream = _fs.createWriteStream(
				filePath,
				{
					flags: 'a',
					mode: parseInt('0644', 8)
				}
			);
	
			if (dataStream.writable) {
				dataStream.write(data + '\n');
			}

			AccessLoggerWriteStreams[logPath] = dataStream;
		} catch (err) {
			AccessLoggerWriteStreams[logPath] = null;
		};
	}
};

/**
 * @private
 * @function
 * @param {SGAppsServerRequest} request 
 * @param {SGAppsServerResponse} response 
 * @param {SGAppsServer} server 
 */
var AccessLoggerHandler = function (server, request, response) {
	var logData = server.AccessLogger.logRequest(request.request, response.response);
	var pathId, loggerPath, updatedLogData = logData, currentLogData;
	var paths = [];
	for (pathId in server.AccessLoggerPaths) {
		if (!updatedLogData) continue;

		loggerPath = server.AccessLoggerPaths[pathId];

		if (loggerPath.isEnabled) {
			if (loggerPath.handle) {
				currentLogData = loggerPath.handle(logData);
			} else {
				currentLogData = updatedLogData;
			}
			
			if (currentLogData) {
				if (loggerPath.waitAllHandlers) {
					updatedLogData = currentLogData;
					if (loggerPath.path) {
						paths.push(loggerPath.path);
					}
				} else {
					if (loggerPath.path) {
						AccessLoggerWriter(loggerPath.path, currentLogData);
					}
				}
			}
		}
	}

	for (pathId in request.AccessLoggerPaths) {
		if (!updatedLogData) continue;

		loggerPath = server.AccessLoggerPaths[pathId];

		if (loggerPath.isEnabled) {
			if (loggerPath.handle) {
				currentLogData = loggerPath.handle(logData);
			} else {
				currentLogData = updatedLogData;
			}
			
			if (currentLogData) {
				if (loggerPath.waitAllHandlers) {
					updatedLogData = currentLogData;
					if (loggerPath.path) {
						paths.push(loggerPath.path);
					}
				} else {
					if (loggerPath.path) {
						AccessLoggerWriter(loggerPath.path, currentLogData);
					}
				}
			}
		}
	}

	if (updatedLogData) {
		paths.forEach(function (path) {
			AccessLoggerWriter(path, updatedLogData);
		});
	}
};

/**
 * this decorator is not enabled by default
 * @memberof SGAppsServerDecoratorsLibrary
 * @method AccessLoggerDecorator
 * @param {SGAppsServerRequest} request 
 * @param {SGAppsServerResponse} response 
 * @param {SGAppsServer} server 
 * @param {function} callback
 */
var AccessLoggerDecorator = function (request, response, server, callback) {
	if (
		request === null
		&& response === null
		&& server
	) {
		/**
		 * @memberof SGAppsServer#
		 * @var {AccessLogger} AccessLogger
		 */
		server.AccessLogger = new AccessLogger();

		/**
		 * @memberof SGAppsServer#
		 * @var {Object<string,AccessLogger.AccessLoggerPath>} AccessLoggerPaths
		 */
		server.AccessLoggerPaths = {};
	} else {
		/**
		 * @memberof SGAppsServerRequest#
		 * @var {Object<string,AccessLogger.AccessLoggerPath>} AccessLoggerPaths
		 */
		request.AccessLoggerPaths = {};
		response.response.on('close', function () {
			AccessLoggerHandler(server, request, response);
		});
	}
	callback();
};

AccessLoggerDecorator.AccessLogger = AccessLogger;

module.exports = AccessLoggerDecorator;