From 1ae3beb3477d31031c80e4739c77acd8ec51cea3 Mon Sep 17 00:00:00 2001 From: Karl-Philipp Wulfert Date: Wed, 27 Mar 2019 16:37:25 +0100 Subject: [PATCH] refactor: rebuild logger to be static Fixes #4 --- src/Logger.ts | 383 +++++++++++++++-------------------------------- src/SMTP.ts | 47 +++--- src/Transport.ts | 27 ++-- src/common.ts | 48 +++++- 4 files changed, 193 insertions(+), 312 deletions(-) diff --git a/src/Logger.ts b/src/Logger.ts index f688048e..178e219f 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 StApps + * Copyright (C) 2018, 2019 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. @@ -12,8 +12,10 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +import chalk from 'chalk'; import {stringify} from 'flatted'; -import {Transport, TransportWithVerification} from './Transport'; +import {isNodeEnvironment, isProductiveNodeEnvironment} from './common'; +import {Transport} from './Transport'; /** * Logger with colors, loglevel and transport @@ -23,189 +25,47 @@ import {Transport, TransportWithVerification} from './Transport'; * * Log levels in that order are: * ``` - * 1 - INFO - * 2 - LOG - * 4 - WARN - * 8 - ERROR - * 16 - OK + * INFO: 1 + * LOG: 2 + * WARN: 4 + * ERROR: 8 + * OK: 16 * ``` */ export class Logger { - - /* - * Reset = "\x1b[0m" - * Bright = "\x1b[1m" - * Dim = "\x1b[2m" - * Underscore = "\x1b[4m" - * Blink = "\x1b[5m" - * Reverse = "\x1b[7m" - * Hidden = "\x1b[8m" - * - * FgBlack = "\x1b[30m" - * FgRed = "\x1b[31m" - * FgGreen = "\x1b[32m" - * FgYellow = "\x1b[33m" - * FgBlue = "\x1b[34m" - * FgMagenta = "\x1b[35m" - * FgCyan = "\x1b[36m" - * FgWhite = "\x1b[37m" - * - * BgBlack = "\x1b[40m" - * BgRed = "\x1b[41m" - * BgGreen = "\x1b[42m" - * BgYellow = "\x1b[43m" - * BgBlue = "\x1b[44m" - * BgMagenta = "\x1b[45m" - * BgCyan = "\x1b[46m" - * BgWhite = "\x1b[47m" - */ - - /** - * Prefix for cyan color - */ - private cyan = '\x1b[36m'; - - /** - * Prefix for green color - */ - private green = '\x1b[32m'; - - /** - * Set to true if this code is executed by Node.js - */ - private isNode: boolean; - - /** - * Set to true if the environment for a productive environment is given - * - * In Node.js this means that `NODE_ENV` is set to `production` - */ - private isProductiveEnvironment: boolean; - /** * Log levels */ - private logLevels: { [logLevel: string]: 1 | 2 | 4 | 8 | 16 } = { - 'INFO': 1, - 'LOG': 2, - 'WARN': 4, - // tslint:disable-next-line:object-literal-sort-keys - 'ERROR': 8, - 'OK': 16, - }; - - /** - * Prefix for red color - */ - private red = '\x1b[31m'; - - /** - * Suffix to end a color - */ - private reset = '\x1b[0m'; + private static logLevels = [ + 'INFO', + 'LOG', + 'WARN', + 'ERROR', + 'OK', + ]; /** * Transport for errors - * - * For example `@stapps/smtp-transport` */ - private transport?: Transport; - - /** - * Prefix for white color - */ - private white = '\x1b[37m'; - - /** - * Prefix for yellow color - */ - private yellow = '\x1b[33m'; - - /** - * Checks if this code is executed in Node.js - */ - public static isNodeEnvironment(): boolean { - // Only Node.js has a process variable that is of [[Class]] process - return Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'; - } - - /** - * Checks if a productive environment is given - */ - public static isProductiveEnvironment(): boolean { - return Logger.isNodeEnvironment && - (typeof process.env.NODE_ENV === 'string' && process.env.NODE_ENV === 'production'); - } - - /** - * Instatiate an instance of logger - * @param transport A transport instance that can be used for error transport - */ - constructor(transport?: Transport) { - // node environment -> maybe we run a service which needs monitoring - if (Logger.isNodeEnvironment()) { - this.isNode = true; - - // check if we are in productive environment -> then we need to run a transport - if (Logger.isProductiveEnvironment()) { - this.isProductiveEnvironment = true; - - if (typeof transport === 'undefined') { - if (process.env.ALLOW_NO_TRANSPORT !== 'true') { - throw new Error('Productive environment doesn\'t set an transport agent for error notifications'); - } else { - this.warn('Productive environment doesn\'t set an transport agent for error notifications'); - } - } else { - - this.transport = transport; - - // we expect an transport for error notifications - if (this.isTransportWithVerification(transport) && !transport.isVerified()) { - transport.verify().then((success) => { - if (typeof success === 'string') { - this.ok(success); - } else { - this.ok('Successfully verified transport for error notification'); - } - }).catch((err) => { - throw err; - }); - } - } - } else { - this.isProductiveEnvironment = false; - } - } else { - this.isProductiveEnvironment = false; - this.isNode = false; - } - } + private static transport?: Transport; /** * Check if intended log level is allowed in environment log level * * @param logLevel - * @returns {boolean} */ - private checkLogLevel(logLevel: 1 | 2 | 4 | 8 | 16) { + private static checkLogLevel(logLevel: string): boolean { + const logLevelNumber = Math.pow(2, Logger.logLevels.indexOf(logLevel) + 1) - 1; - const requiredLogLevel = this.getLogLevel(); - - if (requiredLogLevel > 31 || requiredLogLevel < 0) { - throw new Error('Log level is out of range.'); - } - - // tslint:disable-next-line:no-bitwise - return requiredLogLevel & logLevel; + /* tslint:disable-next-line:no-bitwise */ + return !!(Logger.getLogLevel() & logLevelNumber); } /** * Return log level from environment - * @returns {number} */ - private getLogLevel(): number { - if (this.isNode && typeof process.env.STAPPS_LOG_LEVEL === 'string') { + private static getLogLevel(): number { + if (isNodeEnvironment() && typeof process.env.STAPPS_LOG_LEVEL !== 'undefined') { // Node.js environment exists return parseInt(process.env.STAPPS_LOG_LEVEL, 10); } else if (typeof window !== 'undefined' && typeof (window as any).STAPPS_LOG_LEVEL === 'number') { @@ -217,23 +77,102 @@ export class Logger { return 31; } - private isTransportWithVerification(transport: Transport): transport is TransportWithVerification { - return typeof (transport as any).verify === 'function'; + /** + * Log an error + * + * @param args Arguments to log + */ + public static async error(...args: any[]): Promise { + if (!Logger.checkLogLevel('ERROR')) { + return; + } + + /* tslint:disable-next-line:no-console */ + console.error(chalk.bold.red(`[ERROR] ${Logger.stringifyArguments(...args)}`)); + + if (isProductiveNodeEnvironment()) { + if (typeof Logger.transport !== 'undefined') { + return Logger.transport.send('Error', Logger.stringifyArguments(...args)); + } else if (!process.env.ALLOW_NO_TRANSPORT) { + throw new Error(`Error couldn't be transported! Please set a transport or set ALLOW_NO_TRANSPORT='true'.`); + } + } + } + + /** + * Log an information + * + * @param args Arguments to log + */ + public static info(...args: any[]): void { + if (!Logger.checkLogLevel('INFO')) { + return; + } + + /* tslint:disable-next-line:no-console */ + console.info(chalk.cyan(`[INFO] ${Logger.stringifyArguments(args)}`)); + } + + /** + * Check if the logger is initialized correctly + */ + public static initialized(): void { + if (isProductiveNodeEnvironment() && typeof Logger.transport === 'undefined') { + if (!process.env.ALLOW_NO_TRANSPORT) { + throw new Error(`Productive environment doesn't set a transport for error notifications.`); + } else { + /* tslint:disable-next-line:no-console */ + console.warn(chalk.yellow(`Productive environment doesn't set a transport for error notifications.`)); + } + } + } + + /** + * Log something + * + * @param args Arguments to log + */ + public static log(...args: any[]): void { + if (!this.checkLogLevel('LOG')) { + return; + } + + /* tslint:disable-next-line:no-console */ + console.log(chalk.white(`[LOG] ${Logger.stringifyArguments(args)}`)); + } + + /** + * Log something successful + * + * @param args Arguments to log + */ + public static ok(...args: any[]): void { + if (!this.checkLogLevel('OK')) { + return; + } + + /* tslint:disable-next-line:no-console */ + console.log(chalk.bold.green(`[OK] ${Logger.stringifyArguments(args)}`)); + } + + /** + * Set a transport + * + * @param transport Transport to set + */ + public static setTransport(transport?: Transport) { + Logger.transport = transport; } /** * Stringify a list of arguments * - * @param args {any[]} Arguments to stringify - * @returns {string} Stringified arguments + * @param args Arguments to stringify */ - private stringifyArguments(..._args: any[]): string { + public static stringifyArguments(...args: any[]): string { const result: string[] = []; - /* tslint:disable:prefer-for-of */ - for (let idx = 0; idx < arguments.length; idx++) { - /* tslint:enable */ - const argument = arguments[idx]; + args.forEach((argument) => { const type = typeof argument; if (['string', 'number'].indexOf(type) !== -1) { @@ -246,106 +185,22 @@ export class Logger { } else { result.push(stringify(argument, null, 2)); } - } + }); return result.join(', '); } /** - * Log with level ERROR + * Log a warning * - * @param args {any} Arguments to log + * @param args Arguments to log */ - public error(...args: any[]): void { - if (this.checkLogLevel(this.logLevels.ERROR)) { - if (this.isNode) { - /* tslint:disable-next-line:no-console */ - console.error(this.red + '[ERROR] ' + this.stringifyArguments(...args) + this.reset); - - if (this.isProductiveEnvironment) { - - if (typeof this.transport === 'undefined') { - if (process.env.ALLOW_NO_TRANSPORT !== 'true') { - throw new Error('Error couldn\'t be tranported. Please set an transport or set ALLOW_NO_TRANSPORT=true'); - } - } else { - this.transport.send('Error', this.stringifyArguments(...args)).catch((err) => { - throw err; - }); - } - } - } else { - /* tslint:disable-next-line:no-console */ - console.log('[ERROR] ' + this.stringifyArguments(...args)); - } + public static warn(...args: any[]): void { + if (!this.checkLogLevel('WARN')) { + return; } - } - /** - * Log with level INFO - * - * @param args {any} Arguments to log - */ - public info(...args: any[]): void { - if (this.checkLogLevel(this.logLevels.INFO)) { - if (this.isNode) { - /* tslint:disable-next-line:no-console */ - console.info(this.cyan + '[INFO] ' + this.stringifyArguments(...args) + this.reset); - } else { - /* tslint:disable-next-line:no-console */ - console.info('[INFO] ' + this.stringifyArguments(...args)); - } - } - } - - /** - * Log with level LOG - * - * @param args {any} Arguments to log - */ - public log(...args: any[]): void { - if (this.checkLogLevel(this.logLevels.LOG)) { - if (this.isNode) { - /* tslint:disable-next-line:no-console */ - console.log(this.white + '[LOG] ' + this.stringifyArguments(...args) + this.reset); - } else { - /* tslint:disable-next-line:no-console */ - console.log('[LOG] ' + this.stringifyArguments(...args)); - } - } - } - - /** - * Log with level OK - * - * @param args {any} Arguments to log - */ - public ok(...args: any[]): void { - if (this.checkLogLevel(this.logLevels.OK)) { - if (this.isNode) { - /* tslint:disable-next-line:no-console */ - console.log(this.green + '[OK] ' + this.stringifyArguments(...args) + this.reset); - } else { - /* tslint:disable-next-line:no-console */ - console.log('[OK] ' + this.stringifyArguments(...args)); - } - } - } - - /** - * Log with level WARN - * - * @param args {any} Arguments to log - */ - public warn(...args: any[]): void { - if (this.checkLogLevel(this.logLevels.WARN)) { - if (this.isNode) { - /* tslint:disable-next-line:no-console */ - console.warn(this.yellow + '[WARN] ' + this.stringifyArguments(...args) + this.reset); - } else { - /* tslint:disable-next-line:no-console */ - console.warn('[WARN] ' + this.stringifyArguments(...args)); - } - } + /* tslint:disable-next-line:no-console */ + console.warn(chalk.yellow(`[WARN] ${Logger.stringifyArguments(args)}`)); } } diff --git a/src/SMTP.ts b/src/SMTP.ts index 2fc6af6d..681a5c8f 100644 --- a/src/SMTP.ts +++ b/src/SMTP.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 StApps + * Copyright (C) 2018, 2019 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. @@ -14,9 +14,8 @@ */ import * as nodemailer from 'nodemailer'; import {MailOptions} from 'nodemailer/lib/sendmail-transport'; -import {deleteUndefinedProperties, RecursivePartial} from './common'; -import {Logger} from './Logger'; -import {TransportWithVerification} from './Transport'; +import {deleteUndefinedProperties, isProductiveEnvironment, RecursivePartial} from './common'; +import {VerifiableTransport} from './Transport'; /** * A configuration of the transport used to send mails via SMTP @@ -40,14 +39,14 @@ export interface SMTPConfig { /** * An implementation of mail transport via SMTP */ -export class SMTP extends TransportWithVerification { +export class SMTP extends VerifiableTransport { private static _instance: SMTP; /** * List of all mail addresses to send in cc */ - private cc: string[]; + private readonly cc: string[]; /** * Who is using this service @@ -60,7 +59,7 @@ export class SMTP extends TransportWithVerification { /** * List of all mail addresses to send to */ - private recipients: string[]; + private readonly recipients: string[]; /** * Connection to SMTP server @@ -95,7 +94,7 @@ export class SMTP extends TransportWithVerification { } // monitoring is not required -> SMTP init can fail - if (!Logger.isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT === 'true') { + if (!isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT === 'true') { try { this._instance = new this(config); } catch (err) { @@ -122,14 +121,8 @@ export class SMTP extends TransportWithVerification { * @return {boolean} */ public static isValidEmailAddress(address: string): boolean { - - if (typeof address !== 'string') { - return false; - } - // tslint:disable-next-line:max-line-length - const regex = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; - return regex.test(address); + return /^(([^<>()\[\].,;:\s@"]+(\.[^<>()\[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test(address); } /** @@ -153,13 +146,13 @@ export class SMTP extends TransportWithVerification { password: process.env.SMTP_AUTH_PASSWORD, user: process.env.SMTP_AUTH_USER, }, - cc: ((typeof process.env.SMTP_CC === 'string') ? (process.env.SMTP_CC as string).split(',') : []), + cc: ((typeof process.env.SMTP_CC !== 'undefined') ? (process.env.SMTP_CC as string).split(',') : []), host: process.env.SMTP_HOST, - port: (typeof process.env.SMTP_PORT === 'string') ? parseInt(process.env.SMTP_PORT, 10) : undefined, - recipients: (typeof process.env.SMTP_RECIPIENTS === 'string') ? + port: (typeof process.env.SMTP_PORT !== 'undefined') ? parseInt(process.env.SMTP_PORT, 10) : undefined, + recipients: (typeof process.env.SMTP_RECIPIENTS !== 'undefined') ? (process.env.SMTP_RECIPIENTS).split(',') : [], - secure: (typeof process.env.SMTP_SECURE === 'string') ? (process.env.SMTP_SECURE === 'true') : false, + secure: (typeof process.env.SMTP_SECURE !== 'undefined') ? (process.env.SMTP_SECURE === 'true') : false, sender: { mail: process.env.SMTP_SENDER_MAIL, name: process.env.SMTP_SENDER_NAME, @@ -173,13 +166,13 @@ export class SMTP extends TransportWithVerification { ...deleteUndefinedProperties(envConfig), }; - if (typeof config!.host !== 'string') { + if (typeof config!.host === 'undefined') { throw new Error( 'SMTP configuration needs a host. Add it to the config or use environment variables (SMTP_HOST).', ); } - if (typeof config!.port !== 'number' || isNaN(config!.port)) { + if (typeof config!.port === 'undefined' || isNaN(config!.port)) { throw new Error( 'SMTP configuration needs a port. Add it to the config or use environment variables (SMTP_PORT).', ); @@ -192,13 +185,13 @@ export class SMTP extends TransportWithVerification { ); } - if (typeof config!.auth.user !== 'string') { + if (typeof config!.auth.user === 'undefined') { throw new Error( 'SMTP auth configuration needs a user. Add it to the config or use environment variables (SMTP_AUTH_USER).', ); } - if (typeof config!.auth.password !== 'string') { + if (typeof config!.auth.password === 'undefined') { throw new Error( 'SMTP auth configuration needs a password.' + 'Add it to the config or use environment variables (SMTP_AUTH_PASSWORD).', @@ -211,7 +204,7 @@ export class SMTP extends TransportWithVerification { ); } - if (typeof config!.sender.mail !== 'string') { + if (typeof config!.sender.mail === 'undefined') { throw new Error( 'SMTP configuration needs a sender. Add it to the config or use environment variables (SMTP_SENDER_MAIL).', ); @@ -247,7 +240,7 @@ export class SMTP extends TransportWithVerification { }, host: config!.host, port: config!.port, - secure: typeof config!.secure === 'boolean' ? config!.secure : false, + secure: typeof config!.secure !== 'undefined' ? config!.secure : false, }); } @@ -311,7 +304,7 @@ export class SMTP extends TransportWithVerification { try { verificationSuccessfull = await this.transportAgent.verify(); } catch (err) { - if (!Logger.isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT !== 'true') { + if (!isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT !== 'true') { throw err; } else { /* tslint:disable-next-line:no-console */ @@ -323,7 +316,7 @@ export class SMTP extends TransportWithVerification { } if (!verificationSuccessfull) { - if (!Logger.isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT !== 'true') { + if (!isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT !== 'true') { throw new Error( 'Verification of SMTP transport failed.' + 'If you want to ignore this error set' + diff --git a/src/Transport.ts b/src/Transport.ts index 8f23ed3e..7a2ea33b 100644 --- a/src/Transport.ts +++ b/src/Transport.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 StApps + * Copyright (C) 2018, 2019 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. @@ -14,34 +14,33 @@ */ /** - * An abstract wrapper for a transport system like for example smtp + * An abstract wrapper for a transport system like for example SMTP */ export abstract class Transport { - /** - * Send message with subject. A message can be a mail or something completly different. Depending on what Transport - * is implemented - * @param {string} subject - * @param {string} message + * Send message with subject + * + * A message can be a mail or something completely different depending on what transport is implemented. + * + * @param subject Subject of the message + * @param message Message to send */ abstract send(subject: string, message: string): Promise; - } /** - * A transport wrapper of transport which can be veriefied + * A transport wrapper of transport which can be verified */ -export abstract class TransportWithVerification extends Transport { - +export abstract class VerifiableTransport extends Transport { /** * Checks if the transport was verified at least once - * @returns {boolean} */ abstract isVerified(): boolean; /** - * Verifies transport (check connection, authentication, ...) + * Verifies transport + * + * Check connection, authentication, ... */ abstract verify(): Promise; - } diff --git a/src/common.ts b/src/common.ts index 2bfcc48f..577f9a7c 100644 --- a/src/common.ts +++ b/src/common.ts @@ -13,15 +13,17 @@ * this program. If not, see . */ - /** - * A recursive partial object - * - * Copied from https://stackoverflow.com/a/51365037 - */ +/** + * A recursive partial object + * + * Copied from https://stackoverflow.com/a/51365037 + */ +import {Transport, VerifiableTransport} from './Transport'; + export type RecursivePartial = { [P in keyof T]?: T[P] extends Array ? - Array> : - T[P] extends object ? RecursivePartial : T[P]; + Array> : + T[P] extends object ? RecursivePartial : T[P]; }; /** @@ -47,3 +49,35 @@ export function deleteUndefinedProperties(obj: any) { return obj; } + +/** + * Checks if environment is Node.js + */ +export function isNodeEnvironment(): boolean { + return Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'; +} + +/** + * Checks if environment is productive + */ +export function isProductiveEnvironment(): boolean { + return typeof process.env === 'object' + && typeof process.env.NODE_ENV !== 'undefined' + && process.env.NODE_ENV === 'production'; +} + +/** + * Checks if environment is Node.js and productive + */ +export function isProductiveNodeEnvironment(): boolean { + return isNodeEnvironment() && isProductiveEnvironment(); +} + +/** + * Check if a transport is a verifiable transport + * + * @param transport Transport to check + */ +export function isTransportWithVerification(transport: Transport): transport is VerifiableTransport { + return transport instanceof VerifiableTransport; +}