/* * Copyright (C) 2019-2020 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ import {stringify} from 'flatted'; import {isNodeEnvironment, isProductiveEnvironment, isProductiveNodeEnvironment} from './common.js'; import {Transformation} from './transformation.js'; import {AddLogLevel} from './transformations/add-log-level.js'; import {Transport} from './transport.js'; /** * Check if something has property STAPPS_LOG_LEVEL * @param something Something to check */ // tslint:disable-next-line:completed-docs function hasStAppsLogLevel(something: object): something is {STAPPS_LOG_LEVEL: number} { return 'STAPPS_LOG_LEVEL' in something; } /** * Check if something has property STAPPS_EXIT_LEVEL * @param something Something to check */ // tslint:disable-next-line:completed-docs function hasStAppsExitLevel(something: object): something is {STAPPS_EXIT_LEVEL: number} { return 'STAPPS_EXIT_LEVEL' in something; } /** * A level descriptor for either log or exit level */ export type Level = 'LOG' | 'EXIT'; /** * A log level */ export type LogLevel = 'INFO' | 'LOG' | 'WARN' | 'ERROR' | 'OK'; /** * A logger with transports and transformations * * Log level can be defined by setting the environment variable STAPPS_LOG_LEVEL to a valid log level. Log levels are * set in a binary way. For example STAPPS_LOG_LEVEL=12 does result in logs only for `Logger.warn` and `Logger.error`. * * Log levels in that order are: * ``` * INFO: 1 * LOG: 2 * WARN: 4 * ERROR: 8 * OK: 16 * ``` */ export class Logger { /** * Base of binary system */ private static readonly binaryBase = 2; /** * Log levels */ private static readonly logLevels: LogLevel[] = ['INFO', 'LOG', 'WARN', 'ERROR', 'OK']; /** * Log level sum, equivalent to all log levels enabled */ private static readonly logLevelSum = Math.pow(Logger.binaryBase, Logger.logLevels.length) - 1; /** * Transformers for log output */ private static transformations?: Transformation[] = [new AddLogLevel()]; /** * Transport for errors */ private static transport?: Transport; /** * Apply transformations to an output * Will strip newlines in production environment * @param logLevel Log level of the output * @param output Output to apply transformations to */ private static applyTransformers(logLevel: LogLevel, output: string): string { if (isProductiveEnvironment()) { output = output.replaceAll(/[\n\r]/g, ' '); } if (!Array.isArray(Logger.transformations) || Logger.transformations.length === 0) { return output; } let transformedOutput = output; for (const transformation of Logger.transformations) { transformedOutput = transformation.transform(logLevel, transformedOutput); } return transformedOutput; } /** * Check if intended exit level is allowed in environment exit level * @param exitLevel Log level to check */ private static checkExitLevel(exitLevel: LogLevel): boolean { if (Logger.getLevel('EXIT') === 0) { return false; } // tslint:disable-next-line:no-bitwise return (Logger.getLevel('EXIT') & Logger.logLevelNumber(exitLevel)) === Logger.logLevelNumber(exitLevel); } /** * Check if intended log level is allowed in environment log level * @param logLevel Log level to check */ private static checkLogLevel(logLevel: LogLevel): boolean { // tslint:disable-next-line:no-bitwise return (Logger.getLevel('LOG') & Logger.logLevelNumber(logLevel)) === Logger.logLevelNumber(logLevel); } /** * Notify about exit and end process */ private static exit(): void { if (isProductiveNodeEnvironment()) { return; } // eslint-disable-next-line no-console console.error( Logger.applyTransformers('ERROR', `exiting as of used exit level ${Logger.getLevel('EXIT')} !`), ); process.exit(-1); } /** * Return log level from environment */ private static getLevel(level: Level): number { if (typeof window !== 'undefined') { // browser environment exists if (hasStAppsLogLevel(window)) { return window.STAPPS_LOG_LEVEL; } if (hasStAppsExitLevel(window)) { return window.STAPPS_EXIT_LEVEL; } } const environmentLevel = level === 'LOG' ? process.env.STAPPS_LOG_LEVEL : process.env.STAPPS_EXIT_LEVEL; if (isNodeEnvironment() && environmentLevel !== undefined) { // Node.js environment exists return Number.parseInt(environmentLevel, 10); } // Fallback to log everything, or not exiting switch (level) { case 'LOG': { return Logger.logLevelSum; } case 'EXIT': { return 0; } } } /** * Get number of specific log level * @param logLevel Log level to check */ private static logLevelNumber(logLevel: LogLevel): number { return Math.pow(Logger.binaryBase, Logger.logLevels.indexOf(logLevel)); } /** * Log an error * @param arguments_ Arguments to log */ public static async error(...arguments_: unknown[]): Promise { if (!Logger.checkLogLevel('ERROR')) { return; } // eslint-disable-next-line no-console console.error(Logger.applyTransformers('ERROR', Logger.stringifyArguments(...arguments_))); if (isProductiveNodeEnvironment()) { if (Logger.transport !== undefined) { return Logger.transport.send('Error', Logger.stringifyArguments(...arguments_)); } if (process.env.ALLOW_NO_TRANSPORT !== 'true') { throw new Error( `Error couldn't be transported! Please set a transport or set ALLOW_NO_TRANSPORT='true'.`, ); } } if (Logger.checkExitLevel('ERROR')) { Logger.exit(); } } /** * Log an information * @param arguments_ Arguments to log */ public static info(...arguments_: unknown[]): void { if (!Logger.checkLogLevel('INFO')) { return; } // eslint-disable-next-line no-console console.info(Logger.applyTransformers('INFO', Logger.stringifyArguments(...arguments_))); if (Logger.checkExitLevel('INFO')) { Logger.exit(); } } /** * Check if the logger is initialized correctly */ public static initialized(): void { if (isProductiveNodeEnvironment() && Logger.transport === undefined) { if (process.env.ALLOW_NO_TRANSPORT !== 'true') { throw new Error(`Productive environment doesn't set a transport for error notifications.`); } Logger.warn(`Productive environment doesn't set a transport for error notifications.`); } } /** * Log something * @param arguments_ Arguments to log */ public static log(...arguments_: unknown[]): void { if (!Logger.checkLogLevel('LOG')) { return; } // eslint-disable-next-line no-console console.log(Logger.applyTransformers('LOG', Logger.stringifyArguments(...arguments_))); if (Logger.checkExitLevel('LOG')) { Logger.exit(); } } /** * Log something successful * @param arguments_ Arguments to log */ public static ok(...arguments_: unknown[]): void { if (!Logger.checkLogLevel('OK')) { return; } // eslint-disable-next-line no-console console.log(Logger.applyTransformers('OK', Logger.stringifyArguments(...arguments_))); if (Logger.checkExitLevel('OK')) { Logger.exit(); } } /** * Set transformations for log output * @param transformations List of transformations */ public static setTransformations(transformations: Transformation[]) { const transforms = transformations.filter(transform => isProductiveEnvironment() ? transform.useInProduction === true : true, ); Logger.transformations = transforms; } /** * Set a transport * @param transport Transport to set */ public static setTransport(transport?: Transport) { Logger.transport = transport; } /** * Stringify a list of arguments * @param arguments_ Arguments to stringify */ public static stringifyArguments(...arguments_: unknown[]): string { const result: string[] = []; for (const argument of arguments_) { if (typeof argument === 'string' || typeof argument === 'number') { result.push(argument.toString()); } else if (argument instanceof Error) { result.push(argument.message); if (argument.stack !== undefined) { result.push(argument.stack); } } else { result.push(stringify(argument, undefined, 2)); } } return result.join(', '); } /** * Log a warning * @param arguments_ Arguments to log */ public static warn(...arguments_: unknown[]): void { if (!Logger.checkLogLevel('WARN')) { return; } // eslint-disable-next-line no-console console.warn(Logger.applyTransformers('WARN', Logger.stringifyArguments(...arguments_))); if (Logger.checkExitLevel('WARN')) { Logger.exit(); } } }