diff --git a/app.js b/app.js index 2ea6c14..f7cb04a 100644 --- a/app.js +++ b/app.js @@ -45,6 +45,9 @@ try { } config._dir = __dirname; +// Enable logging module +require('./src/log')(config); + // Port if (config['env-port'] && process.env.PORT) { config.port = process.env.PORT; @@ -56,7 +59,7 @@ if (!config['env-region'] && process.env.region) { } if (config.region.length > 10 || !config.region.match(/^[a-z0-9_-]+$/i)) { config.region = ''; - console.log('Invalid value for region config variable.'); + config.log('Invalid value for region config variable.', 'config-region', true); } // Reload secret key @@ -84,7 +87,7 @@ function loadIcons(firstLoad) { return new Promise((fulfill, reject) => { function getCollections() { let t = Date.now(), - newCollections = new Collections(config, console.log); + newCollections = new Collections(config); console.log('Loading collections at ' + (new Date()).toString()); newCollections.reload(dirs.getRepos()).then(() => { @@ -142,7 +145,7 @@ function reloadIcons(firstLoad) { reloadIcons(false); } }).catch(err => { - console.log('Fatal error loading collections:', err); + config.log('Fatal error loading collections:\n' + util.format(err), null, true); loading = false; if (anotherReload) { reloadIcons(false); @@ -258,7 +261,7 @@ loadIcons(true).then(newCollections => { }, 30000); } }).catch(err => { - console.log('Fatal error loading collections:', err); + config.log('Fatal error loading collections:\n' + util.format(err), null, true); loading = false; reloadIcons(true); }); @@ -363,7 +366,7 @@ app.get('/sync', (req, res) => { } } }).catch(err => { - console.log(err); + config.log('Error synchronizing repository "' + repo + '":\n' + util.format(err), 'sync-' + repo, true); }); } diff --git a/config-default.json b/config-default.json index f153412..ca9d6e5 100644 --- a/config-default.json +++ b/config-default.json @@ -29,5 +29,22 @@ "simple-svg": "git@github.com:simplesvg/icons.git", "custom": "", "custom-dir": "" + }, + "mail": { + "active": false, + "throttle": 30, + "repeat": 180, + "from": "noreply@localhost", + "to": "noreply@localhost", + "subject": "SimpleSVG icons log", + "transport": { + "host": "smtp.ethereal.email", + "port": 587, + "secure": false, + "auth": { + "user": "username", + "pass": "password" + } + } } } \ No newline at end of file diff --git a/config.md b/config.md index 75227c9..fdf641f 100644 --- a/config.md +++ b/config.md @@ -147,3 +147,44 @@ URL of custom icons repository. Location of json files in custom repository, relative to root directory of repository. For example, if json files are located in directory "json" in your repository (like they are in simple-svg repository), set custom-dir value to "json". + + +## Logging errors + +Server can automatically email you if something happens, so you don't need to check logs. + +Email configuration is in "mail" object of config.json. To activate email logging set "mail.active" to "true", set correct from and to addresses and SMTP settings. + +#### active + +Set to true to enable logging to email. + +#### throttle + +Number of seconds to delay email sending. + +Default is 30 seconds. All error messages within 30 seconds will be combined to one email instead of sending multiple emails. + +#### repeat + +This option prevents script from sending similar errors too often. Value is number of minutes. Default value is 180 (3 hours). + +#### from + +Sender email address. Set this to valid email address. + +#### to + +Received email address. Set this to valid email address. + +#### subject + +Subject of emails. All emails will have same subject. + +If you are running SimpleSVG icons app on multiple servers, use different subjects for different servers to identify which server email came from. + +#### transport + +SMTP settings. + +If you are using secure connection, set "secure" to true and "port" to 465, unless you are running SMTP server on different port. diff --git a/package-lock.json b/package-lock.json index ec8ce85..f0ad399 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "simple-svg-website-icons", - "version": "1.0.0-beta2", + "version": "1.0.0-beta3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -417,6 +417,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "nodemailer": { + "version": "4.6.8", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.8.tgz", + "integrity": "sha512-A3s7EM/426OBIZbLHXq2KkgvmKbn2Xga4m4G+ZUA4IaZvG8PcZXrFh+2E4VaS2o+emhuUVRnzKN2YmpkXQ9qwA==" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/package.json b/package.json index 64da354..c6f1ad6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "express": "^4.16.3", + "nodemailer": "^4.6.8", "simple-svg-icons": "git+https://github.com/simplesvg/icons.git" }, "devDependencies": { diff --git a/src/collections.js b/src/collections.js index ce775f1..a146f03 100644 --- a/src/collections.js +++ b/src/collections.js @@ -11,10 +11,8 @@ class Collections { * Constructor * * @param {object} config Application configuration - * @param {function} [log] Logging function */ - constructor(config, log) { - this._log = typeof log === 'function' ? log : null; + constructor(config) { this._config = config; this.items = {}; @@ -139,7 +137,7 @@ class Collections { * Load queue * * Promise will never reject because single file should not break app, - * it will log failures using "log" function from constructor + * it will log failures instead * * @returns {Promise} */ @@ -166,9 +164,7 @@ class Collections { total += count; } }); - if (this._log !== null) { - this._log('Loaded ' + total + ' icons'); - } + console.log('Loaded ' + total + ' icons'); fulfill(this); }).catch(err => { reject(err); @@ -188,9 +184,7 @@ class Collections { return new Promise((fulfill, reject) => { fs.readdir(dir, (err, files) => { if (err) { - if (this._log !== null) { - this._log('Error loading directory: ' + dir); - } + this._config.log('Error reading directory: ' + dir + '\n' + util.format(err), 'collections-' + dir, true); fulfill(false); } else { let promises = []; @@ -241,34 +235,26 @@ class Collections { collection.loadFile(filename, prefix).then(result => { collection = result; if (!collection.loaded) { - if (this._log !== null) { - this._log('Failed to loadQueue collection: ' + filename); - } + this._config.log('Failed to load collection: ' + filename, 'collection-load-' + filename, true); fulfill(false); return; } if (collection.prefix !== prefix) { - if (this._log !== null) { - this._log('Collection prefix does not match: ' + collection.prefix + ' in file ' + file); - } + this._config.log('Collection prefix does not match: ' + collection.prefix + ' in file ' + filename, 'collection-prefix-' + filename, true); fulfill(false); return; } let count = Object.keys(collection.icons).length; if (!count) { - if (this._log !== null) { - this._log('Collection is empty: ' + file); - } + this._config.log('Collection is empty: ' + filename, 'collection-empty-' + filename, true); fulfill(false); return; } this.items[prefix] = collection; - if (this._log !== null) { - this._log('Loaded collection ' + prefix + ' from ' + file + ' (' + count + ' icons)'); - } + console.log('Loaded collection ' + prefix + ' from ' + file + ' (' + count + ' icons)'); fulfill(count); }).catch(() => { fulfill(false); diff --git a/src/log.js b/src/log.js new file mode 100644 index 0000000..e55cf08 --- /dev/null +++ b/src/log.js @@ -0,0 +1,89 @@ +"use strict"; + +const nodemailer = require('nodemailer'); + +/** + * Inject logging function as config.log() + * + * @param config + */ +module.exports = config => { + if (config.mail && config.mail.active) { + let logged = {}, + mailError = false, + throttled = false, + throttledData = [], + repeat = Math.max(config.mail.repeat, 15) * 60 * 1000; // convert minutes to ms, no less than 15 minutes + + /** + * Send messages queue + */ + let send = () => { + throttled = false; + + // Create transport + let transporter = nodemailer.createTransport(config.mail.transport); + + // Mail options + let mailOptions = { + from: config.mail.from, + to: config.mail.to, + subject: config.mail.subject, + text: throttledData.join('\n\n- - - - - - - - - - -\n\n') + }; + throttledData = []; + + // Send email + transporter.sendMail(mailOptions, (err, info) => { + if (err) { + if (mailError === false) { + console.error('Error sending mail (this messages will not show up again on further email errors until app is restarted):'); + console.error(err); + mailError = true; + } + } + }); + }; + + console.log('Logging to email is active. If you do not receive emails with errors, check configuration options.'); + + /** + * + * @param {string} message + * @param {string} [key] Unique key to identify logging message to avoid sending too many duplicate emails + * @param {boolean} [copyToConsole] True if log should be copied to console + */ + config.log = (message, key, copyToConsole) => { + if (copyToConsole) { + console.error('\x1b[31m' + message + '\x1b[0m'); + } + + // Do not send same email more than once within "repeat" minutes + let time = Date.now() / repeat; + if (typeof key === 'string') { + if (logged[key] === time) { + return; + } + logged[key] = time; + } + + // Throttle + throttledData.push(message); + if (config.mail.throttle) { + if (!throttled) { + throttled = true; + setTimeout(send, config.mail.throttle * 1000); + } + } else { + send(); + } + }; + } else { + console.log('Logging to email is not active.'); + config.log = (message, key, copyToConsole) => { + if (copyToConsole) { + console.error('\x1b[35m' + message + '\x1b[0m'); + } + }; + } +}; diff --git a/src/sync.js b/src/sync.js index 36ad91d..b24ff50 100644 --- a/src/sync.js +++ b/src/sync.js @@ -58,7 +58,7 @@ const startSync = repo => { uid: process.getuid() }, (error, stdout, stderr) => { if (error) { - console.error('Error executing git', error); + config.log('Error executing git:' + util.format(error), cmd, true); done(false); return; } @@ -81,9 +81,9 @@ const startSync = repo => { */ const removeFile = file => new Promise((fulfill, reject) => { fs.unlink(file, err => { - // if (err) { - // console.log(err); - // } + if (err) { + config.log('Error deleting file ' + file, file, false); + } fulfill(); }) }); @@ -97,9 +97,7 @@ const removeFile = file => new Promise((fulfill, reject) => { const removeDir = dir => new Promise((fulfill, reject) => { function done() { fs.rmdir(dir, err => { - // if (err) { - // console.log(err); - // } + config.log('Error deleting directory ' + dir, dir, false); fulfill(); }); } @@ -136,7 +134,7 @@ const removeDir = dir => new Promise((fulfill, reject) => { }).then(() => { done(); }).catch(err => { - console.log(err); + config.log('Error recursively removing directory ' + dir + '\n' + util.format(err), 'rmdir-' + dir, true); done(); }); }); @@ -193,7 +191,7 @@ const functions = { fs.writeFile(_versionsFile, JSON.stringify(data, null, 4), 'utf8', err => { if (err) { - console.error('Error saving versions.json', err); + config.error('Error saving versions.json\n' + util.format(err), 'version-' + _versionsFile, true); } }); }, @@ -309,7 +307,7 @@ const functions = { promiseQueue(dirs, dir => removeDir(dir)).then(() => { cleaning = false; }).catch(err => { - console.log(err); + config.log('Error cleaning up old files:\n' + util.format(err), 'cleanup', true); cleaning = false; }); }); @@ -387,7 +385,7 @@ function init() { } catch (err) { } - console.error('Error loading latest collections: directory does not exist:', dir); + config.log('Error loading latest collections: directory does not exist: ' + dir, 'missing-' + dir, true); }); setTimeout(functions.cleanup, 60000); }