├── .gitignore ├── .npmignore ├── README.md ├── lib ├── broker │ └── index.js ├── config │ └── index.js ├── constants │ └── index.js ├── eventListeners │ └── index.js ├── index.js ├── interceptors │ └── index.js ├── logs │ └── index.js ├── metrics │ └── index.js ├── middlewares │ └── index.js ├── request │ └── index.js ├── snippets │ └── index.js └── utils │ ├── extractStacktrace.js │ ├── index.js │ └── timestamp.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Log Owl adapter for NodeJS 2 | 3 | Adapter to monitor NodeJS web servers. 4 | 5 | ## Usage 6 | 7 | Install the adapter with `npm i --save @logowl/adapter-nodejs` or `yarn add @logowl/adapter-nodejs`. 8 | 9 | ## Configuration 10 | 11 | Require the adapter at the top of your server and pass the configuration to the `init` function. 12 | 13 | ```javascript 14 | const logowl = require('@logowl/adapter-nodejs'); 15 | 16 | logowl.init({ ticket: '2ATNP1AD70' }); 17 | ``` 18 | 19 | ## Documentation 20 | 21 | Visit our [documentation](https://docs.logowl.io/docs/nodejs-adapter) for more. -------------------------------------------------------------------------------- /lib/broker/index.js: -------------------------------------------------------------------------------- 1 | const constants = require('../constants/'); 2 | const capturedLogs = require('../logs/'); 3 | const snippets = require('../snippets/'); 4 | const request = require('../request/'); 5 | const metrics = require('../metrics'); 6 | const config = require('../config/'); 7 | const utils = require('../utils/'); 8 | 9 | /** 10 | * Registers and sends an error. 11 | * @param message {string} error message 12 | * @param stack {string} stacktrace of the error 13 | * @param constructor {object} constructor of the error object 14 | * @param onTheFlyBadges {object} dynamically added badges if the error was emitted manually 15 | */ 16 | const error = async ({ message, stack, constructor }, onTheFlyBadges = {}) => { 17 | try { 18 | if (!stack || !config.isSetup()) { 19 | return; 20 | } 21 | 22 | message = message.length >= 1000 ? message.slice(0, 995) + '...' : message; 23 | stack = stack.length >= 15000 ? stack.slice(0, 14995) + '...' : stack; 24 | 25 | const ticket = config.get().ticket; 26 | const badges = config.get().badges; 27 | const logs = capturedLogs.get(); 28 | const machineMetrics = metrics.load(); 29 | const { path, line } = utils.extractStacktrace(stack); 30 | const snippet = snippets.getSnippet(path, line); 31 | const timestamp = utils.timestamp.generateUTCInSeconds(); 32 | 33 | const event = { 34 | ticket, 35 | message, 36 | badges: { ...badges, ...onTheFlyBadges }, 37 | stacktrace: stack, 38 | adapter: constants.adapter, 39 | type: (constructor && constructor.name) || 'error', 40 | metrics: machineMetrics, 41 | logs, 42 | snippet, 43 | path, 44 | line, 45 | timestamp 46 | }; 47 | 48 | await request(event, 'error'); 49 | 50 | } catch (error) { 51 | console.error(constants.colors.red + 'Failed to register Log Owl event with error:\n\n' + error + constants.colors.reset); 52 | } 53 | }; 54 | 55 | module.exports = { error }; -------------------------------------------------------------------------------- /lib/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stores the configuration provided by the user. 3 | * @returns {{set: set, get: (function(): {}), isSetup: (function(): boolean)}} 4 | */ 5 | const config = () => { 6 | let _config = {}; 7 | 8 | 9 | /** 10 | * Sets and updates the configuration. 11 | * @param conf {object} the configuration object 12 | * @returns {boolean} indicates if the configuration was set successfully. 13 | */ 14 | const set = (conf) => { 15 | if (!(conf instanceof Object && conf.constructor === Object && conf.ticket)) { 16 | return false; 17 | } 18 | 19 | _config = conf; 20 | 21 | return true; 22 | }; 23 | 24 | 25 | /** 26 | * Gets and returns the current configuration. 27 | * @returns {{}} 28 | */ 29 | const get = () => _config; 30 | 31 | 32 | /** 33 | * Indicates if the configuration is set correctly. 34 | * @returns {boolean} 35 | */ 36 | const isSetup = () => Boolean(_config && _config.ticket); 37 | 38 | 39 | return { set, isSetup, get }; 40 | }; 41 | 42 | module.exports = config(); -------------------------------------------------------------------------------- /lib/constants/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | colors: { 3 | red: '\u001b[31m', 4 | reset: '\u001b[0m' 5 | }, 6 | connectivity: { 7 | serviceURL: 'https://api.logowl.io' 8 | }, 9 | adapter: { 10 | name: 'logowl-adapter-nodejs', 11 | type: 'nodejs', 12 | version: 'v2.0.3' 13 | } 14 | }; -------------------------------------------------------------------------------- /lib/eventListeners/index.js: -------------------------------------------------------------------------------- 1 | const broker = require('../broker/'); 2 | 3 | /** 4 | * Enables event listeners to register uncaught exceptions 5 | * as well as unhandled promise rejections. 6 | */ 7 | const enableAll = () => { 8 | process.on('uncaughtException', async (error) => { 9 | await broker.error(error); 10 | console.error(error); 11 | process.exit(1); 12 | }); 13 | 14 | process.on('unhandledRejection', async (error) => { 15 | await broker.error(error); 16 | console.error(error); 17 | process.exit(1); 18 | }); 19 | }; 20 | 21 | module.exports = { enableAll }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const eventListeners = require('./eventListeners/'); 2 | const interceptors = require('./interceptors/'); 3 | const middlewares = require('./middlewares'); 4 | const constants = require('./constants/'); 5 | const config = require('./config/'); 6 | const broker = require('./broker'); 7 | 8 | /** 9 | * Initializes the adapter with the user provided configuration. 10 | * @param conf {object} provided configuration 11 | */ 12 | const init = (conf) => { 13 | const isValid = config.set(conf); 14 | 15 | if (isValid) { 16 | eventListeners.enableAll(); 17 | interceptors.enableAll(); 18 | } else { 19 | console.error(constants.colors.red + 'The provided Log Owl configuration is invalid.' + constants.colors.reset); 20 | } 21 | }; 22 | 23 | const emitError = (error, onTheFlyBadges = {}) => { 24 | if (!error || !(error instanceof Error)) { 25 | console.error('[LOG OWL] the provided error must be an instance of Error'); 26 | return; 27 | } 28 | 29 | if (typeof onTheFlyBadges !== 'object' || Array.isArray(onTheFlyBadges)) { 30 | console.error('[LOG OWL] on the fly badges must be an object'); 31 | return; 32 | } 33 | 34 | if (Object.keys(onTheFlyBadges || {}).some(b => typeof onTheFlyBadges[b] !== 'string')) { 35 | console.error('[LOG OWL] on the fly badges need to be an object of strings'); 36 | return; 37 | } 38 | 39 | broker.error(error, onTheFlyBadges); 40 | }; 41 | 42 | module.exports = { init, errorHandler: middlewares.errorHandler, emitError }; -------------------------------------------------------------------------------- /lib/interceptors/index.js: -------------------------------------------------------------------------------- 1 | const logs = require('../logs'); 2 | 3 | const enableAll = () => { 4 | const originalStdoutWrite = process.stdout.write.bind(process.stdout); 5 | 6 | process.stdout.write = (chunk, encoding, callback) => { 7 | if (typeof chunk === 'string') { 8 | logs.add(chunk); 9 | } 10 | 11 | return originalStdoutWrite(chunk, encoding, callback); 12 | }; 13 | }; 14 | 15 | module.exports = { enableAll }; -------------------------------------------------------------------------------- /lib/logs/index.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils/'); 2 | 3 | /** 4 | * Saves and returns logs. 5 | * @returns {{add: add, get: (function(): [])}} 6 | */ 7 | const logs = () => { 8 | const logs = []; 9 | 10 | /** 11 | * Register a new log and ensures that only a 12 | * maximum of 10 logs are stored at a time. 13 | * @param log {string} the log that should be saved. 14 | */ 15 | const add = (log) => { 16 | if (typeof log !== 'string') { 17 | return; 18 | } 19 | 20 | log = log.length >= 1000 ? log.slice(0, 995) + '...' : log; 21 | 22 | logs.push({ timestamp: utils.timestamp.generateUTCInSeconds(), type: 'log', log }); 23 | 24 | if (logs.length > 10) { 25 | logs.shift(); 26 | } 27 | }; 28 | 29 | /** 30 | * Returns all logs that are currently stored. 31 | * @returns {[]} 32 | */ 33 | const get = () => logs; 34 | 35 | return { add, get }; 36 | }; 37 | 38 | module.exports = logs(); -------------------------------------------------------------------------------- /lib/metrics/index.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | const load = () => ({ 4 | platform: os.platform() 5 | }); 6 | 7 | module.exports = {load}; -------------------------------------------------------------------------------- /lib/middlewares/index.js: -------------------------------------------------------------------------------- 1 | const broker = require('../broker/'); 2 | 3 | const errorHandler = (error, req, res, next) => { 4 | broker.error(error); 5 | 6 | next(error); 7 | }; 8 | 9 | module.exports = { errorHandler }; -------------------------------------------------------------------------------- /lib/request/index.js: -------------------------------------------------------------------------------- 1 | const superagent = require('superagent'); 2 | 3 | const constants = require('../constants/'); 4 | const config = require('../config/'); 5 | 6 | /** 7 | * Sends an event to the service. 8 | * @param event {object} the event that should be sent to the service. 9 | * @param eventType {string} the type of the event 10 | * @returns {*} 11 | */ 12 | const request = (event, eventType) => { 13 | const userConfig = config.get(); 14 | 15 | let url = constants.connectivity.serviceURL; 16 | 17 | if (userConfig.endpoint) { 18 | url = userConfig.endpoint; 19 | } 20 | 21 | return superagent.post(url + '/logging/' + eventType) 22 | .retry(2) 23 | .send(event) 24 | .set('Content-Type', 'application/json'); 25 | }; 26 | 27 | module.exports = request; 28 | 29 | -------------------------------------------------------------------------------- /lib/snippets/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | /** 4 | * Caches snippets to prevent redundant file parsing. 5 | * @type {Map} 6 | */ 7 | const cache = new Map(); 8 | 9 | /** 10 | * Loads a file and extracts the code at a given position. 11 | * @param path {string} file path 12 | * @param line {string} the line in which the error occurred 13 | * @returns {string[]} extracted code 14 | */ 15 | const getSnippet = (path, line) => { 16 | const cachedSnippet = cache.get(path + line); 17 | 18 | if (cachedSnippet) { 19 | return cachedSnippet; 20 | } 21 | 22 | const file = fs.readFileSync(path); 23 | const formattedFile = file.toString().split('\n'); 24 | 25 | const snippet = {}; 26 | 27 | for (let i = line - 6; i < (parseInt(line) + 5); i++) { 28 | if (formattedFile[i] !== undefined) { 29 | snippet[i + 1] = ((formattedFile[i] || '').length > 795) ? formattedFile[i].slice(0, 795) + '...' : formattedFile[i]; 30 | } 31 | } 32 | 33 | cache.set(path + line, snippet); 34 | 35 | return snippet; 36 | }; 37 | 38 | module.exports = { getSnippet }; -------------------------------------------------------------------------------- /lib/utils/extractStacktrace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracts the file path and line number from the stack trace. 3 | * @param stack {string} error stack trace 4 | * @returns {{path: string, line: string}} file path and line number 5 | */ 6 | const extractStacktrace = (stack) => { 7 | const [, path, line] = stack.match(/\/([\/\w-_\.]+\.js):(\d*):(\d*)/); 8 | 9 | return { path: '/' + path, line }; 10 | }; 11 | 12 | module.exports = extractStacktrace; -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | const extractStacktrace = require('./extractStacktrace'); 2 | const timestamp = require('./timestamp'); 3 | 4 | module.exports = { extractStacktrace, timestamp }; -------------------------------------------------------------------------------- /lib/utils/timestamp.js: -------------------------------------------------------------------------------- 1 | const generateUTCInSeconds = () => Math.floor(Date.now() / 1000); 2 | 3 | module.exports = { generateUTCInSeconds }; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@logowl/adapter-nodejs", 3 | "version": "0.1.5", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "asynckit": { 8 | "version": "0.4.0", 9 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 10 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 11 | }, 12 | "combined-stream": { 13 | "version": "1.0.8", 14 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 15 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 16 | "requires": { 17 | "delayed-stream": "~1.0.0" 18 | } 19 | }, 20 | "component-emitter": { 21 | "version": "1.3.0", 22 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", 23 | "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" 24 | }, 25 | "cookiejar": { 26 | "version": "2.1.2", 27 | "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", 28 | "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" 29 | }, 30 | "debug": { 31 | "version": "4.1.1", 32 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 33 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 34 | "requires": { 35 | "ms": "^2.1.1" 36 | } 37 | }, 38 | "delayed-stream": { 39 | "version": "1.0.0", 40 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 41 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 42 | }, 43 | "fast-safe-stringify": { 44 | "version": "2.0.7", 45 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", 46 | "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" 47 | }, 48 | "form-data": { 49 | "version": "3.0.0", 50 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", 51 | "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", 52 | "requires": { 53 | "asynckit": "^0.4.0", 54 | "combined-stream": "^1.0.8", 55 | "mime-types": "^2.1.12" 56 | } 57 | }, 58 | "formidable": { 59 | "version": "1.2.2", 60 | "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", 61 | "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" 62 | }, 63 | "inherits": { 64 | "version": "2.0.4", 65 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 66 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 67 | }, 68 | "methods": { 69 | "version": "1.1.2", 70 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 71 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 72 | }, 73 | "mime": { 74 | "version": "2.4.4", 75 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", 76 | "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" 77 | }, 78 | "mime-db": { 79 | "version": "1.43.0", 80 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", 81 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" 82 | }, 83 | "mime-types": { 84 | "version": "2.1.26", 85 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", 86 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", 87 | "requires": { 88 | "mime-db": "1.43.0" 89 | } 90 | }, 91 | "ms": { 92 | "version": "2.1.2", 93 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 94 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 95 | }, 96 | "qs": { 97 | "version": "6.9.3", 98 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", 99 | "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==" 100 | }, 101 | "readable-stream": { 102 | "version": "3.6.0", 103 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 104 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 105 | "requires": { 106 | "inherits": "^2.0.3", 107 | "string_decoder": "^1.1.1", 108 | "util-deprecate": "^1.0.1" 109 | } 110 | }, 111 | "safe-buffer": { 112 | "version": "5.2.0", 113 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 114 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 115 | }, 116 | "semver": { 117 | "version": "6.3.0", 118 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 119 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 120 | }, 121 | "string_decoder": { 122 | "version": "1.3.0", 123 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 124 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 125 | "requires": { 126 | "safe-buffer": "~5.2.0" 127 | } 128 | }, 129 | "superagent": { 130 | "version": "5.2.2", 131 | "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.2.2.tgz", 132 | "integrity": "sha512-pMWBUnIllK4ZTw7p/UaobiQPwAO5w/1NRRTDpV0FTVNmECztsxKspj3ZWEordVEaqpZtmOQJJna4yTLyC/q7PQ==", 133 | "requires": { 134 | "component-emitter": "^1.3.0", 135 | "cookiejar": "^2.1.2", 136 | "debug": "^4.1.1", 137 | "fast-safe-stringify": "^2.0.7", 138 | "form-data": "^3.0.0", 139 | "formidable": "^1.2.1", 140 | "methods": "^1.1.2", 141 | "mime": "^2.4.4", 142 | "qs": "^6.9.1", 143 | "readable-stream": "^3.4.0", 144 | "semver": "^6.3.0" 145 | } 146 | }, 147 | "util-deprecate": { 148 | "version": "1.0.2", 149 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 150 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@logowl/adapter-nodejs", 3 | "version": "2.0.3", 4 | "description": "Adapter to monitor NodeJS web servers.", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jz222/logowl-adapter-nodejs.git" 12 | }, 13 | "author": "Timo Zimmermann", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/jz222/logowl-adapter-nodejs/issues" 17 | }, 18 | "homepage": "https://github.com/jz222/logowl-adapter-nodejs#readme", 19 | "keywords": [ 20 | "logowl", 21 | "adapter", 22 | "nodejs", 23 | "monitoring" 24 | ], 25 | "dependencies": { 26 | "superagent": "5.2.2" 27 | } 28 | } 29 | --------------------------------------------------------------------------------