├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images └── poop.png ├── lib ├── index.js └── utils.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | npm-debug.log 4 | dump.rdb 5 | node_modules 6 | results.tap 7 | results.xml 8 | npm-shrinkwrap.json 9 | config.json 10 | .DS_Store 11 | */.DS_Store 12 | */*/.DS_Store 13 | ._* 14 | */._* 15 | */*/._* 16 | coverage.* 17 | lib-cov 18 | 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | !.npmignore 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | env: 4 | - CXX="g++-4.8" 5 | node_js: 6 | - "8" 7 | - "10" 8 | - "11" 9 | - "node" 10 | addons: 11 | apt: 12 | sources: 13 | - ubuntu-toolchain-r-test 14 | packages: 15 | - g++-4.8 16 | - gcc-4.8 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Breaking changes are documented using GitHub issues, see [issues labeled "release notes"](https://github.com/hapijs/poop/issues?q=is%3Aissue+label%3A%22release+notes%22). 2 | 3 | If you want changes of a specific minor or patch release, you can browse the [GitHub milestones](https://github.com/hapijs/poop/milestones?state=closed&direction=asc&sort=due_date). 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016, Project contributors 2 | Copyright (c) 2014-2015, Walmart and other contributors. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * The names of any contributors may not be used to endorse or promote 13 | products derived from this software without specific prior written 14 | permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | * * * 28 | The complete list of contributors can be found at: https://github.com/hapijs/poop/graphs/contributors 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![poop Logo](https://raw.github.com/hapijs/poop/master/images/poop.png) 2 | 3 | [`hapi`](https://github.com/hapijs/hapi) plugin for taking a process dump and cleaning up after an uncaught exception 4 | 5 | [![Current Version](https://img.shields.io/npm/v/poop.svg)](https://www.npmjs.org/package/poop) 6 | [![Build Status](https://secure.travis-ci.org/hapijs/poop.png)](http://travis-ci.org/hapijs/poop) 7 | 8 | Lead Maintainer: [Colin Ihrig](https://github.com/cjihrig) 9 | 10 | ## Table of Contents 11 | 12 | - [Overview](#overview) 13 | - [Example](#example) 14 | - [Settings](#settings) 15 | - [`heapdumpFolder`](#heapdumpfolder) 16 | - [`logPath`](#logpath) 17 | - [`writeStreamOptions`](#writestreamoptions) 18 | 19 | ## Overview 20 | 21 | When an `uncaughtException` is encountered a heap dump will be output to the 22 | plugin folder. A log file is also written containing the exception details. 23 | After this is complete the process will exit with a failure code. Heap dumps can 24 | also be taken at arbitrary times by sending a `SIGUSR1` signal to the server 25 | process. 26 | 27 | ## Example 28 | 29 | The following example shows how to register and configure `poop`. In this example, 30 | an uncaught exception is thrown after `poop` is registered. This will trigger a 31 | heap dump and `poop.log` file to be created. 32 | 33 | ```javascript 34 | 'use strict'; 35 | var Path = require('path'); 36 | var Hapi = require('hapi'); 37 | var Poop = require('poop'); 38 | var server = new Hapi.Server(); 39 | 40 | server.register({ 41 | register: Poop, 42 | options: { 43 | logPath: Path.join(__dirname, 'poop.log') 44 | } 45 | }, function () { 46 | 47 | throw new Error('uncaught'); 48 | }); 49 | ``` 50 | 51 | ## Settings 52 | 53 | The following options are available when configuring `poop`. 54 | 55 | ### `heapdumpFolder` 56 | 57 | The directory to place heap dump files. Defaults to the process current working directory. 58 | 59 | ### `logPath` 60 | 61 | The file path to log any uncaught exception errors. Defaults to `poop.log` in 62 | the process current working directory. 63 | 64 | ### `writeStreamOptions` 65 | 66 | Options passed to the write stream of the log file. Uses Node's defaults: 67 | 68 | ```javascript 69 | { 70 | flags: 'w', 71 | encoding: null, 72 | fd: null, 73 | mode: 0666 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /images/poop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outmoded/poop/b691c390a642bb09ebde8b8d0503aee2770c6a45/images/poop.png -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Path = require('path'); 6 | const HeapDump = require('heapdump'); 7 | const Utils = require('./utils'); 8 | 9 | 10 | // Declare internals 11 | 12 | const internals = { 13 | initialized: false, 14 | defaults: { 15 | logPath: Path.join(process.cwd(), 'poop.log'), 16 | heapdumpFolder: process.cwd(), 17 | writeStreamOptions: { flags: 'w' } 18 | } 19 | }; 20 | 21 | 22 | const register = function (server, options) { 23 | 24 | const settings = Object.assign({}, internals.defaults, options); 25 | 26 | if (internals.initialized) { 27 | return; 28 | } 29 | 30 | internals.initialized = true; 31 | 32 | process.once('uncaughtException', async (err) => { 33 | 34 | HeapDump.writeSnapshot(Path.join(settings.heapdumpFolder, 'heapdump-' + Date.now() + '.heapsnapshot')); 35 | 36 | // Don't try to catch any errors since the process is exiting anyway. 37 | await Utils.log(err, { 38 | logPath: settings.logPath, 39 | writeStreamOptions: settings.writeStreamOptions 40 | }); 41 | 42 | process.exit(1); 43 | }); 44 | 45 | process.on('SIGUSR1', () => { 46 | 47 | HeapDump.writeSnapshot(Path.join(settings.heapdumpFolder, 'heapdump-' + Date.now() + '.heapsnapshot')); 48 | }); 49 | }; 50 | 51 | 52 | module.exports = { 53 | pkg: require('../package.json'), 54 | requirements: { 55 | hapi: '>=17.0.0' 56 | }, 57 | register 58 | }; 59 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Fs = require('fs'); 6 | const Os = require('os'); 7 | const Path = require('path'); 8 | const Util = require('util'); 9 | const Mkdirp = require('mkdirp'); 10 | const Teamwork = require('teamwork'); 11 | 12 | 13 | // Declare internals 14 | 15 | const internals = { 16 | mkdirp: Util.promisify(Mkdirp) 17 | }; 18 | 19 | 20 | module.exports.log = async function (err, options) { 21 | 22 | await internals.mkdirp(Path.dirname(options.logPath)); 23 | const log = Fs.createWriteStream(options.logPath, options.writeStreamOptions); 24 | const formattedErr = { 25 | message: err.message, 26 | stack: err.stack, 27 | timestamp: Date.now() 28 | }; 29 | const team = new Teamwork(); 30 | 31 | log.write(JSON.stringify(formattedErr) + Os.EOL, () => { 32 | 33 | log.end(); 34 | team.attend(); 35 | }); 36 | 37 | await team.work; 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poop", 3 | "version": "3.0.1", 4 | "description": "Hapi plugin for taking a process dump on uncaught exception", 5 | "main": "lib/index.js", 6 | "homepage": "https://github.com/hapijs/poop", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/hapijs/poop.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/hapijs/poop/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "dependencies": { 16 | "heapdump": "0.3.x", 17 | "mkdirp": "0.5.1", 18 | "teamwork": "3.x.x" 19 | }, 20 | "devDependencies": { 21 | "code": "5.x.x", 22 | "hapi": "18.x.x", 23 | "lab": "18.x.x" 24 | }, 25 | "scripts": { 26 | "test": "lab -t 100 -v -L -a code", 27 | "test-cov-html": "lab -a code -r html -o coverage.html" 28 | }, 29 | "keywords": [ 30 | "hapi", 31 | "exception", 32 | "dump", 33 | "plugin" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Fs = require('fs'); 6 | const Os = require('os'); 7 | const Path = require('path'); 8 | const Code = require('code'); 9 | const Hapi = require('hapi'); 10 | const Lab = require('lab'); 11 | const Teamwork = require('teamwork'); 12 | const Poop = require('../lib'); 13 | const PoopUtils = require('../lib/utils'); 14 | 15 | 16 | // Declare internals 17 | 18 | const internals = { 19 | logPath: Path.join(__dirname, 'test.log') 20 | }; 21 | 22 | 23 | // Test shortcuts 24 | 25 | const lab = exports.lab = Lab.script(); 26 | const { describe, it } = lab; 27 | const expect = Code.expect; 28 | 29 | 30 | describe('Poop', () => { 31 | 32 | lab.afterEach(() => { 33 | 34 | internals.cleanup(); 35 | }); 36 | 37 | it('logs uncaught exceptions, takes dump, and exits process', async () => { 38 | 39 | const team = new Teamwork(); 40 | const orig = process.exit; 41 | 42 | process.removeAllListeners('uncaughtException'); 43 | await internals.prepareServer(); 44 | process.exit = function (code) { 45 | 46 | process.exit = orig; 47 | expect(code).to.equal(1); 48 | 49 | const exception = JSON.parse(Fs.readFileSync(internals.logPath, 'utf8')); 50 | 51 | expect(exception.message).to.equal('test'); 52 | expect(exception.stack).to.be.a.string(); 53 | expect(exception.timestamp).to.be.a.number(); 54 | Fs.unlinkSync(internals.logPath); 55 | expect(internals.countHeaps()).to.equal(1); 56 | team.attend(); 57 | }; 58 | 59 | process.emit('uncaughtException', new Error('test')); 60 | await team.work; 61 | }); 62 | 63 | it('takes dump on SIGUSR1 events', async () => { 64 | 65 | await internals.prepareServer(); 66 | process.emit('SIGUSR1'); 67 | expect(internals.countHeaps()).to.equal(1); 68 | }); 69 | 70 | it('configures the the log file', async () => { 71 | 72 | const err1 = new Error('test 1'); 73 | const err2 = new Error('test 2'); 74 | const options = { 75 | logPath: Path.join(__dirname, 'config.log'), 76 | writeStreamOptions: { flags: 'a' } 77 | }; 78 | 79 | await PoopUtils.log(err1, options); 80 | await PoopUtils.log(err2, options); 81 | 82 | const exceptions = Fs.readFileSync(options.logPath, 'utf8').split(Os.EOL); 83 | const ex1 = JSON.parse(exceptions[0]); 84 | const ex2 = JSON.parse(exceptions[1]); 85 | 86 | expect(ex1.message).to.equal('test 1'); 87 | expect(ex1.stack).to.be.a.string(); 88 | expect(ex1.timestamp).to.be.a.number(); 89 | expect(ex2.message).to.equal('test 2'); 90 | expect(ex2.stack).to.be.a.string(); 91 | expect(ex2.timestamp).to.be.a.number(); 92 | Fs.unlinkSync(options.logPath); 93 | }); 94 | 95 | it('can register the plugin multiple times', async () => { 96 | 97 | const server1 = await internals.prepareServer(); 98 | expect(server1).to.exist(); 99 | const server2 = await internals.prepareServer(); 100 | expect(server2).to.exist(); 101 | }); 102 | 103 | it('can handle null options in register()', () => { 104 | 105 | expect(() => { 106 | 107 | Poop.register({}, null); 108 | }).to.not.throw(); 109 | }); 110 | }); 111 | 112 | 113 | internals.prepareServer = async function () { 114 | 115 | const server = Hapi.server(); 116 | 117 | await server.register({ 118 | plugin: Poop, 119 | options: { logPath: internals.logPath } 120 | }); 121 | 122 | return server; 123 | }; 124 | 125 | 126 | internals.countHeaps = function () { 127 | 128 | const files = Fs.readdirSync(process.cwd()); 129 | let count = 0; 130 | 131 | for (let i = 0; i < files.length; ++i) { 132 | if (files[i].indexOf('heapdump-') === 0) { 133 | count++; 134 | } 135 | } 136 | 137 | return count; 138 | }; 139 | 140 | 141 | internals.cleanup = function () { 142 | 143 | const files = Fs.readdirSync(process.cwd()); 144 | 145 | for (let i = 0; i < files.length; ++i) { 146 | if (files[i].indexOf('heapdump-') === 0) { 147 | Fs.unlinkSync(Path.join(process.cwd(), files[i])); 148 | } 149 | } 150 | }; 151 | --------------------------------------------------------------------------------