├── examples ├── logs │ └── .gitignore ├── pids │ └── .gitignore ├── basic.js ├── multiple-email.js ├── default.js └── template.js ├── index.js ├── .gitignore ├── History.md ├── Makefile ├── package.json ├── views ├── basic.jade ├── history.jade └── default.jade ├── tests └── cluster.exception.test.js ├── lib ├── format.js ├── history.js └── cluster.exception.js └── Readme.md /examples/logs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /examples/pids/.gitignore: -------------------------------------------------------------------------------- 1 | *.pid -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/cluster.exception'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | prototypes 4 | *.sock 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.0.1 / 2010-11-03 2 | ================== 3 | 4 | * Initial release 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | expresso -I lib $(TESTFLAGS) tests/*.test.js 3 | 4 | .PHONY: test -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , cluster = require('cluster') 3 | , exception = require('../'); 4 | 5 | var app = http.createServer(function(req, res){ 6 | res.writeHead(200); 7 | res.end("hello world"); 8 | }); 9 | 10 | cluster(app) 11 | .use(cluster.logger('logs')) 12 | .use(cluster.stats()) 13 | .use(cluster.pidfiles('pids')) 14 | .use(cluster.cli()) 15 | .use(cluster.repl(8888)) 16 | .use(exception({to: 'info+cluster.exception@3rd-Eden.com'})) 17 | .listen(8080) -------------------------------------------------------------------------------- /examples/multiple-email.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , cluster = require('cluster') 3 | , exception = require('../'); 4 | 5 | var app = http.createServer(function(req, res){ 6 | res.writeHead(200); 7 | res.end("hello world"); 8 | }); 9 | 10 | cluster(app) 11 | .use(cluster.logger('logs')) 12 | .use(cluster.stats()) 13 | .use(cluster.pidfiles('pids')) 14 | .use(cluster.cli()) 15 | .use(cluster.repl(8888)) 16 | .use(exception({to: ['info+cluster.exception@3rd-Eden.com', 'hello+cluster.exception@3rd-Eden.com']})) 17 | .listen(8080) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cluster.exception" 3 | , "version": "0.0.3" 4 | , "description": "Exception handling for cluster.js" 5 | , "keywords": ["cluster", "mail", "exception", "error"] 6 | , "author": "Arnout Kazemier " 7 | , "dependencies": { 8 | "email": "0.2.x" 9 | , "jade": "0.10.x" 10 | } 11 | , "devDependencies": { 12 | "expresso": "" 13 | , "should": "" 14 | , "cluster": "" 15 | } 16 | , "main": "index" 17 | , "respository": { 18 | "type": "git" 19 | , "url": "https://3rd-Eden@github.com/3rd-Eden/cluster.exception.git" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/default.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , cluster = require('cluster') 3 | , exception = require('../'); 4 | 5 | var app = http.createServer(function httpServer(req, res){ 6 | res.writeHead(200); 7 | res.end("hello world"); 8 | if(req.url.match('favicon')){ 9 | console.info('Im a console.info'); 10 | console.warn('Im a console.warn'); 11 | console.log('Im a console.log'); 12 | console.error('Im a console.error'); 13 | console.info(req); 14 | 15 | throw new Error("Omfg, uncaught error"); 16 | } 17 | }); 18 | 19 | cluster = cluster(app) 20 | .use(cluster.stats()) 21 | .use(cluster.pidfiles('pids')) 22 | .use(cluster.cli()) 23 | .use(cluster.repl(8888)) 24 | .use(exception({to: 'info+cluster.exception@3rd-Eden.com'})) 25 | .listen(8080); 26 | -------------------------------------------------------------------------------- /examples/template.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , cluster = require('cluster') 3 | , exception = require('../'); 4 | 5 | var app = http.createServer(function httpServer(req, res){ 6 | res.writeHead(200); 7 | res.end("hello world"); 8 | if(req.url.match('favicon')){ 9 | console.info('Im a console.info'); 10 | console.warn('Im a console.warn'); 11 | console.log('Im a console.log'); 12 | console.error('Im a console.error'); 13 | console.info(req); 14 | 15 | throw new Error("Omfg, uncaught error"); 16 | } 17 | }); 18 | 19 | cluster = cluster(app) 20 | .use(cluster.stats()) 21 | .use(cluster.pidfiles('pids')) 22 | .use(cluster.cli()) 23 | .use(cluster.repl(8888)) 24 | .use(exception({to: 'info+cluster.exception@3rd-Eden.com', template:'history'})) 25 | .listen(8080); 26 | -------------------------------------------------------------------------------- /views/basic.jade: -------------------------------------------------------------------------------- 1 | html 2 | body(style="background:#fff; font: 14px/1.4 'helvetica neue', helvetica, arial, sans-serif; margin:0; padding:100px; color:#222") 3 | h1(style="font-size:28px; margin-top:25px") Cluster exception 4 | p An uncaught exception occurred in script 5 | span(style="font-family: monospace; color:#3B88D8; margin: 0 7px;")= environment.arguments[1] 6 | | on 7 | span(style="font-family: monospace; color:#3B88D8; margin: 0 7px;")= date 8 | | with the message 9 | span(style="font-family: monospace; color:#3B88D8; margin-left:7px;")= exception.message || "no message" 10 | | . 11 | 12 | h2(style="font-size:16px; margin-top:20px") Stacktrace 13 | pre(style="border:1px solid #eee;padding:10px;min-width:800px;overflow:auto")#{exception.stack} 14 | 15 | h2(style="font-size:16px; margin-top:20px") Cluster instance 16 | - Object.keys(cluster).forEach(function(key){ 17 | dl 18 | dt(style="font-family: monospace; color:#3B88D8; display:inline-block; width:200px")= key 19 | dd(style="display:inline")= cluster[key] 20 | - }); -------------------------------------------------------------------------------- /tests/cluster.exception.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var exception = require('../') 6 | , should = require('should') 7 | , cluster = require('cluster') 8 | , History = require('../lib/history') 9 | 10 | module.exports = { 11 | 'test .version': function(){ 12 | exception.version.should.match(/^\d+\.\d+\.\d+$/); 13 | }, 14 | 15 | 'history module constructor': function(){ 16 | var history = new History({duration:1000}); 17 | history.keys.should.be.an.instanceof(Array); 18 | history.duration.should.eql(1000); 19 | 20 | history.should.respondTo('destroy'); 21 | history.should.respondTo('update'); 22 | history.should.respondTo('toGraph'); 23 | 24 | history.destroy(); 25 | }, 26 | 27 | 'history module data generation': function(){ 28 | var history = new History({duration:1000}); 29 | 30 | setTimeout(function(){ 31 | history.keys.forEach(function(key){ 32 | history.data[key].should.have.length(2); 33 | }); 34 | 35 | history.destroy(); 36 | }, (history.duration * 2) + 100) 37 | }, 38 | 39 | 'history module graph generation': function(){ 40 | var history = new History({duration:100}); 41 | 42 | setTimeout(function(){ 43 | history.keys.forEach(function(key){ 44 | var result = history.toGraph(key); 45 | result.should.be.an.instanceof(Object); 46 | 47 | Object.keys(result).forEach(function(key){ 48 | if (result[key]) result[key].should.have.string('google'); 49 | }); 50 | }); 51 | 52 | history.destroy(); 53 | }, (history.duration * 12) + 100); 54 | }, 55 | 56 | 'exception constructor': function(){ 57 | var plugin; 58 | try{ plugin = exception() }catch(e){ e.should.be.an.instanceof(Error) } 59 | try{ plugin = exception({}) }catch(e){ e.should.be.an.instanceof(Error) } 60 | 61 | plugin = exception({ to:'info@3rd-Eden.com' }); 62 | plugin.should.be.an.instanceof(Function); 63 | }, 64 | 65 | 'exception accessible for workers': function(){ 66 | var plugin = exception({ to:'info@3rd-Eden.com' }); 67 | plugin.enableInWorker.should.be.ok 68 | } 69 | }; -------------------------------------------------------------------------------- /views/history.jade: -------------------------------------------------------------------------------- 1 | html 2 | body(style="background:#fff; font: 14px/1.4 'helvetica neue', helvetica, arial, sans-serif; margin:0; padding:100px; color:#222") 3 | h1(style="font-size:28px; margin-top:25px") Cluster exception 4 | p An uncaught exception occurred in script 5 | span(style="font-family: monospace; color:#3B88D8; margin: 0 7px;")= environment.arguments[1] 6 | | on 7 | span(style="font-family: monospace; color:#3B88D8; margin: 0 7px;")= date 8 | | with the message 9 | span(style="font-family: monospace; color:#3B88D8; margin-left:7px;")= exception.message || "no message" 10 | | . 11 | 12 | h2(style="font-size:16px; margin-top:20px") Stacktrace 13 | pre(style="border:1px solid #eee;padding:10px;min-width:800px;overflow:auto")#{exception.stack} 14 | 15 | h2(style="font-size:16px; margin-top:20px") Cluster instance 16 | - Object.keys(cluster).forEach(function(key){ 17 | dl 18 | dt(style="font-family: monospace; color:#3B88D8; display:inline-block; width:200px")= key 19 | dd(style="display:inline")= cluster[key] 20 | - }); 21 | 22 | h2(style="font-size:16px; margin-top:20px") Log snapshot 23 | - Object.keys(log).forEach(function(key){ 24 | - if(key && log[key].length){ 25 | h2(style="font-family: monospace; font-size:14px; color:#3B88D8;")= key 26 | pre(style="border:1px solid #eee;padding:10px;min-width:800px;overflow:auto")= log[key] 27 | - } 28 | - }); 29 | 30 | - var mem = Object.keys(graph).filter(function(v){return v.indexOf('mem_') >= 0 }); 31 | - if(mem.length){ 32 | h2(style="font-size:16px; margin-top:20px") Memory history 33 | - mem.forEach(function(key){ 34 | img(src:graph[key], style="margin:0 40px 40px 0; border:1px solid #fafafa") 35 | - }); 36 | - } 37 | 38 | - var load = Object.keys(graph).filter(function(v){return v.indexOf('load_') >= 0 }); 39 | - if(load.length){ 40 | h2(style="font-size:16px; margin-top:20px") Load average history 41 | - load.forEach(function(key){ 42 | img(src: graph[key], style="margin:0 40px 40px 0; border:1px solid #fafafa") 43 | - }); 44 | - } 45 | 46 | - var cpus = Object.keys(graph).filter(function(v){return v.indexOf('cpu_') >= 0 }); 47 | - if(cpus.length){ 48 | h2(style="font-size:16px; margin-top:20px") CPU Cycle history 49 | - cpus.forEach(function(key){ 50 | img(src: graph[key], style="margin:0 40px 40px 0; border:1px solid #fafafa") 51 | - }); 52 | -} 53 | -------------------------------------------------------------------------------- /lib/format.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Joyent, Inc. and other Node contributors. 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the 7 | * "Software"), to deal in the Software without restriction, including 8 | * without limitation the rights to use, copy, modify, merge, publish, 9 | * distribute, sublicense, and/or sell copies of the Software, and to permit 10 | * persons to whom the Software is furnished to do so, subject to the 11 | * following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included 14 | * in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 19 | * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 20 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 21 | * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 22 | * USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | * 24 | * code copy / inspiration from https://github.com/joyent/node/blob/master/lib/console.js 25 | */ 26 | 27 | /** 28 | * Module dependencies 29 | */ 30 | var util = require('util') 31 | , formatRegExp = /%[sdj]/g; 32 | 33 | /** 34 | * Formats arguments to a output string 35 | * 36 | * @param {Arguments} arguments The arguments that need to be formatted 37 | * 38 | * @returns {String} 39 | * @api public 40 | */ 41 | function format(f) { 42 | if (typeof f !== 'string') { 43 | for (var objects = [], i = 0, length = arguments.length; i < length; i++) { 44 | objects.push(util.inspect(arguments[i])); 45 | } 46 | return objects.join(' '); 47 | } 48 | 49 | var i = 1 50 | , args = arguments 51 | , length = args.length 52 | , str = String(f).replace(formatRegExp, function(x) { 53 | switch (x) { 54 | case '%s': return String(args[i++]); 55 | case '%d': return Number(args[i++]); 56 | case '%j': return JSON.stringify(args[i++]); 57 | default: 58 | return x; 59 | } 60 | }); 61 | 62 | for (var x = args[i]; i < length; x = args[++i]) { 63 | if (x === null || typeof x !== 'object') { 64 | str += ' ' + x; 65 | } else { 66 | str += ' ' + util.inspect(x); 67 | } 68 | } 69 | 70 | return str; 71 | }; 72 | 73 | /** 74 | * Generate a new timestamp 75 | * 76 | * @returns {String} Timestamp in `26 Feb 16:19:34` format 77 | * @api public 78 | */ 79 | function timestamp() { 80 | var d = new Date() 81 | , pad = timestamp.pad 82 | , months = timestamp.months 83 | , time = [ 84 | pad(d.getHours()) 85 | , pad(d.getMinutes()) 86 | , pad(d.getSeconds()) 87 | ].join(':'); 88 | 89 | return [d.getDate(), months[d.getMonth()], time].join(' '); 90 | }; 91 | 92 | /** 93 | * Added a leading zero infront of numbers when 94 | * they are less than 10 95 | * 96 | * @param {Number} n The number that needs to be padded 97 | * 98 | * @returns {String} a padded string 99 | * @api public 100 | */ 101 | timestamp.pad = function pad(n) { 102 | return n < 10 ? '0' + n.toString(10) : n.toString(10); 103 | }; 104 | 105 | /** 106 | * A array with months, for the timestamp function 107 | * 108 | * @type {Object} 109 | * @api public 110 | */ 111 | timestamp.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 112 | 113 | exports.timestamp = timestamp; 114 | exports.format = format; -------------------------------------------------------------------------------- /views/default.jade: -------------------------------------------------------------------------------- 1 | html 2 | body(style="background:#fff; font: 14px/1.4 'helvetica neue', helvetica, arial, sans-serif; margin:0; padding:100px; color:#222") 3 | h1(style="font-size:28px; margin-top:25px") Cluster exception 4 | p An uncaught exception occurred in script 5 | span(style="font-family: monospace; color:#3B88D8; margin: 0 7px;")= environment.arguments[1] 6 | | on 7 | span(style="font-family: monospace; color:#3B88D8; margin: 0 7px;")= date 8 | | with the message 9 | span(style="font-family: monospace; color:#3B88D8; margin-left:7px;")= exception.message || "no message" 10 | | . 11 | 12 | h2(style="font-size:16px; margin-top:20px") Stacktrace 13 | pre(style="border:1px solid #eee;padding:10px;min-width:800px;overflow:auto")#{exception.stack} 14 | 15 | h2(style="font-size:16px; margin-top:20px") Cluster instance 16 | - Object.keys(cluster).forEach(function(key){ 17 | dl 18 | dt(style="font-family: monospace; color:#3B88D8; display:inline-block; width:200px")= key 19 | dd(style="display:inline")= cluster[key] 20 | - }); 21 | 22 | h2(style="font-size:16px; margin-top:20px") Log snapshot 23 | - Object.keys(log).forEach(function(key){ 24 | - if(key && log[key].length){ 25 | h2(style="font-family: monospace; font-size:14px; color:#3B88D8;")= key 26 | pre(style="border:1px solid #eee;padding:10px;min-width:800px;overflow:auto")= log[key] 27 | - } 28 | - }); 29 | 30 | - var mem = Object.keys(graph).filter(function(v){return v.indexOf('mem_') >= 0 }); 31 | - if(mem.length){ 32 | h2(style="font-size:16px; margin-top:20px") Memory history 33 | - mem.forEach(function(key){ 34 | img(src:graph[key], style="margin:0 40px 40px 0; border:1px solid #fafafa") 35 | - }); 36 | - } 37 | 38 | - var load = Object.keys(graph).filter(function(v){return v.indexOf('load_') >= 0 }); 39 | - if(load.length){ 40 | h2(style="font-size:16px; margin-top:20px") Load average history 41 | - load.forEach(function(key){ 42 | img(src: graph[key], style="margin:0 40px 40px 0; border:1px solid #fafafa") 43 | - }); 44 | - } 45 | 46 | - var cpus = Object.keys(graph).filter(function(v){return v.indexOf('cpu_') >= 0 }); 47 | - if(cpus.length){ 48 | h2(style="font-size:16px; margin-top:20px") CPU Cycle history 49 | - cpus.forEach(function(key){ 50 | img(src: graph[key], style="margin:0 40px 40px 0; border:1px solid #fafafa") 51 | - }); 52 | -} 53 | 54 | h2(style="font-size:16px; margin-top:20px") Operating System 55 | - Object.keys(os).forEach(function(key){ 56 | dl 57 | dt(style="font-family: monospace; color:#3B88D8; display:inline-block; width:200px")= key 58 | dd(style="display:inline")= os[key] 59 | - }); 60 | 61 | h2(style="font-size:16px; margin-top:20px") Library versions 62 | - Object.keys(environment.versions).forEach(function(key){ 63 | dl 64 | dt(style="font-family: monospace; color:#3B88D8; display:inline-block; width:200px")= key 65 | dd(style="display:inline")= environment.versions[key] 66 | - }); 67 | 68 | h2(style="font-size:16px; margin-top:20px") Process identification 69 | - ['pid', 'gid', 'uid'].forEach(function(key){ 70 | dl 71 | dt(style="font-family: monospace; color:#3B88D8; display:inline-block; width:200px")= key 72 | dd(style="display:inline")= environment[key] 73 | - }); 74 | 75 | h2(style="font-size:16px; margin-top:20px") Environment variables 76 | - Object.keys(environment.env).forEach(function(key){ 77 | dl 78 | dt(style="font-family: monospace; color:#3B88D8; display:inline-block; width:200px")= key 79 | dd(style="display:inline")= environment.env[key] 80 | - }); -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # cluster.exception 2 | 3 | ``` 4 | NOTE: This module has been deprecated, please use something else. 5 | ``` 6 | 7 | Exception notification plugin for Cluster 8 | 9 | ## Installation 10 | 11 | The easiest way to install the module is through the node package manager (npm). 12 | 13 | > npm install cluster.exception 14 | 15 | Or you could clone this github repository and point your require statement to that. 16 | 17 | > git clone git://github.com/3rd-Eden/cluster.exception.git 18 | 19 | ## Usage 20 | 21 | Options: 22 | 23 | - `from` sender email address. _Optional, should be a string. Defaults to cluster@dev.null._ 24 | - `to` receiving email addresses. _Required, a array or string._ 25 | - `subject` email subject. _Optional, string, can contain optional template tags. Defaults to Cluster.exception {date}._ 26 | - `methods` console.* methods that need to be monitored. _Optional, array. Defaults to ['log','info','warn','error']._ 27 | - `template` verbosity of the email content. _Optional, string. Defaults to default. Can either be default, basic or history._ 28 | - `history` options for the History metrics module. _Optional, object._ 29 | - `limit` The amount samples it should store internally. _Optional, number. Defaults to 50._ 30 | - `duration` The interval of the snapshots. _Optional, number in ms. Defaults to 25 seconds (25000 ms)._ 31 | 32 | ## Example 33 | 34 | ``` js 35 | var http = require('http') 36 | , cluster = require('cluster') 37 | , exception = require('../'); 38 | 39 | var app = http.createServer(function httpServer(req, res){ 40 | res.writeHead(200); 41 | res.end("hello world"); 42 | if(req.url.match('favicon')){ 43 | console.info('Im a console.info'); 44 | console.warn('Im a console.warn'); 45 | console.log('Im a console.log'); 46 | console.error('Im a console.error'); 47 | console.info(req); 48 | 49 | throw new Error("Omfg, uncaught error"); 50 | } 51 | }); 52 | 53 | cluster = cluster(app) 54 | .use(cluster.stats()) 55 | .use(cluster.pidfiles('pids')) 56 | .use(cluster.cli()) 57 | .use(cluster.repl(8888)) 58 | .use(exception({to: 'your-email@ddress.here'})) 59 | .listen(8080); 60 | ``` 61 | 62 | Or check the [examples](https://github.com/3rd-Eden/cluster.exception/tree/master/examples) folder for more examples. 63 | 64 | ## Templates 65 | 66 | - `basic` This includes the **Stracktrace** and **Cluster instance** information. 67 | - `history` This includes the **Stracktrace**, **Cluster instance**, **Log snapshot** and **graphs**. 68 | - `default` This includes.. Everything you see in the screenshot below, you can never have to much information. 69 | 70 | ## Screenshots 71 | 72 | ![](http://dl.dropbox.com/u/1381492/shots/screeny-github-cluster-exception.png) 73 | 74 | ## Roadmap 75 | 76 | The initial release will only contain support for email notifications. In the next iteration there will be support for multiple backends available. There are some use cases where you would want to store the details of the exception + the context in a database or somewhere else. So a configurable backend is something that would make a fine addition to the plugin. 77 | 78 | Once this has been realized, I will rip out the `History` module and create a new `node-metrics` module from it, so it will be completely customizable and reusable with only the metrics that you think are important. If you have application that does allot database queries, it might be useful to know see if the queries per second where increasing, or that it took to long for your database server to respond. The possibilities are endless. 79 | 80 | ## License 81 | 82 | (The MIT License) 83 | 84 | Copyright (c) 2011 Arnout Kazemier <info@3rd-Eden.com> 85 | 86 | Permission is hereby granted, free of charge, to any person obtaining 87 | a copy of this software and associated documentation files (the 88 | 'Software'), to deal in the Software without restriction, including 89 | without limitation the rights to use, copy, modify, merge, publish, 90 | distribute, sublicense, and/or sell copies of the Software, and to 91 | permit persons to whom the Software is furnished to do so, subject to 92 | the following conditions: 93 | 94 | The above copyright notice and this permission notice shall be 95 | included in all copies or substantial portions of the Software. 96 | 97 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 98 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 99 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. 100 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 101 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 102 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 103 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 104 | -------------------------------------------------------------------------------- /lib/history.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /*! 4 | * cluster.exception 5 | * Copyright(c) 2011 Arnout Kazemier 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies 11 | */ 12 | var os = require('os'); 13 | 14 | /** 15 | * The `History` function takes snapshots of the cpu / memory to 16 | * create a more realisic overview of the of what was happing before 17 | * an error got triggered as some errors might be related to CPU spikes 18 | * and other operating system based details. 19 | * 20 | * @constructor 21 | * @param {Object} options Configurable option set 22 | * @param {Number} options.limit The maximum log length before we start removing older entries 23 | * @param {Number} options.duration Amount in miliseconds that a snapshot should be taken 24 | * 25 | * @api private 26 | */ 27 | 28 | var History = module.exports = function History (options) { 29 | var self = this; 30 | 31 | this.limit = 50; 32 | this.duration = 1000 * 25; 33 | 34 | // overwite our data with options, if needed 35 | if (options) { 36 | for (var key in options) this[key] = options[key]; 37 | } 38 | 39 | this.stats = { 40 | 'loadaverage': os.loadavg 41 | , 'cpus': os.cpus 42 | , 'freemem': os.freemem 43 | , 'memory': process.memoryUsage 44 | }; 45 | 46 | this.data = {}; 47 | this.totalmem = History.bytesToMb(os.totalmem()); 48 | this.keys = Object.keys(this.stats); 49 | 50 | // initiate the History 51 | this.keys.forEach(function createDataStructure (key) { 52 | self.data[key] = []; 53 | }); 54 | 55 | this.interval = setInterval(function historyUpdate () { 56 | self.update.call(self); 57 | }, this.duration); 58 | }; 59 | 60 | /** 61 | * Updates the data with a new stat dump. If the data exceeds the allowed 62 | * limit we will remove the a old item from the data structure so we will 63 | * have a fresh feed of data. 64 | * 65 | * @api private 66 | */ 67 | 68 | History.prototype.update = function update () { 69 | var i = this.keys.length 70 | , key; 71 | 72 | while (i--) { 73 | key = this.keys[i]; 74 | 75 | this.data[key].push( this.stats[key]() ); 76 | if (this.data[key].length > this.limit) this.data[key].shift(); 77 | } 78 | }; 79 | 80 | /** 81 | * Exports the gathered history as a `Google chart` graph. 82 | * 83 | * @param {String} key The stat that needs to be converted to a chart 84 | * 85 | * @returns {Object} Key => url 86 | * @api public 87 | */ 88 | 89 | History.prototype.toGraph = function toGraph (key) { 90 | var url = 'https://chart.googleapis.com/chart?cht=ls&chs=400x150&chts=222222,14' 91 | , base = url + '&chco=3B88D8&chd=' 92 | , data = this.data[key] 93 | , i = data.length 94 | , points = [] 95 | , result = {} 96 | , tmp = {}; 97 | 98 | switch (key) { 99 | case 'memory': 100 | ['rss', 'vsize', 'heapUsed', 'heapTotal'].forEach(function generate (type) { 101 | points.length = 0; 102 | i = data.length; 103 | 104 | if (i) { 105 | while (i--) points.push(History.bytesToMb(data[i][type])); 106 | 107 | tmp.sorted = points.sort(function sortTimes (a,b) { 108 | return b - a; 109 | }); 110 | 111 | tmp.high = tmp.sorted[0]; 112 | 113 | result['mem_' + type] = base + 't:' + points.join(',') + '&chtt=Process+memory+usage+'+ type + '+(MB)' + 114 | '&chds=0,' + tmp.high + 115 | '&chxr=0,0,' + tmp.high +'&chxt=r'; 116 | } 117 | }); 118 | break; 119 | 120 | case 'freemem': 121 | if (i){ 122 | while (i--) points.push(this.totalmem - History.bytesToMb(data[i])); 123 | 124 | tmp.sorted = points.sort(function sortTimes (a,b) { 125 | return b - a; 126 | }); 127 | 128 | tmp.high = tmp.sorted[0]; 129 | 130 | result['mem_' + key] = base + 't:' + points.join(',') + '&chtt=Total+memory+(limit+'+ this.totalmem+'+MB)' + 131 | '&chds=0,' + this.totalmem + 132 | '&chxr=0,0,' + this.totalmem +'&chxt=r'; 133 | } 134 | break; 135 | 136 | case 'cpus': 137 | // no data, return.. nothing 138 | if (!data[0] || !i) return result; 139 | 140 | data[0].forEach(function getCPUNames (cpu, index) { 141 | var name = 'CPU ' + ( index + 1) + ' ' + cpu.model 142 | , time; 143 | 144 | tmp[name] = {}; 145 | 146 | for (time in cpu.times) { 147 | tmp[name][time] = []; 148 | } 149 | }); 150 | 151 | while (i--) { 152 | data[i].forEach(function getCPUTimes (cpu, index) { 153 | var name = 'CPU ' + ( index + 1) + ' ' + cpu.model 154 | , time; 155 | 156 | for (time in cpu.times) { 157 | tmp[name][time].push(cpu.times[time]); 158 | } 159 | }); 160 | } 161 | 162 | Object.keys(tmp).forEach(function assempleCPUResult (cpu) { 163 | var times = tmp[cpu] 164 | , time; 165 | 166 | tmp.labels = []; 167 | tmp.counts = []; 168 | tmp.lowest = 9E99; 169 | tmp.highest = 0; 170 | 171 | for (time in times) { 172 | if (time === 'nice' || time === 'irq') continue; 173 | 174 | tmp.sorted = times[time].sort(function sortTimes (a,b) { 175 | return b - a; 176 | }); 177 | 178 | tmp.high = tmp.sorted[0]; 179 | tmp.low = tmp.sorted[ tmp.sorted.length - 1 ]; 180 | tmp.low = tmp.low === tmp.high ? 0 : tmp.low; 181 | 182 | tmp.lowest = tmp.lowest > tmp.low ? tmp.low : tmp.lowest; 183 | tmp.highest = tmp.highest > tmp.high ? tmp.highest : tmp.high; 184 | 185 | tmp.counts.push(times[time].join(',')); 186 | tmp.labels.push(time); 187 | } 188 | 189 | result['cpu_' + cpu ] = url + '&chd=t:' + tmp.counts.join('|') + '&chdl=' + tmp.labels.join('|') + 190 | '&chco=DA3B15,F7A10A,4582E7,579F3A,9100E5&chxt=r&chdlp=t&chtt=' + cpu.split(' ').join('+') + 191 | '&chds=' + tmp.lowest + ',' + tmp.highest + 192 | '&chxr=0,' + tmp.lowest + ',' + tmp.highest; 193 | }); 194 | break; 195 | 196 | case 'loadaverage': 197 | if (i) { 198 | tmp.labels = { 199 | '0': 'Server load 1 minute interval' 200 | , '1': 'Server load 5 minutes interval' 201 | , '2': 'Server load 15 minutes interval' 202 | }; 203 | 204 | Object.keys(tmp.labels).forEach(function (key) { 205 | points = []; 206 | i = data.length; 207 | 208 | while (i--) { 209 | points.push(data[i][key]); 210 | } 211 | 212 | tmp.sorted = points.sort(function sortTimes (a,b) { 213 | return b - a; 214 | }); 215 | 216 | tmp.high = tmp.sorted[0]; 217 | 218 | result['load_' + key] = base + 't:' + points.join(',') + '&chtt='+ tmp.labels[key] + 219 | '&chds=0,' + tmp.high + 220 | '&chxr=0,0,' + tmp.high +'&chxt=r'; 221 | }); 222 | } 223 | break; 224 | } 225 | 226 | return result; 227 | }; 228 | 229 | /** 230 | * Clear the running interval and clean the recoreded 231 | * data. 232 | * 233 | * @api public 234 | */ 235 | 236 | History.prototype.destroy = function destroy () { 237 | clearInterval(this.interval); 238 | this.data = null; 239 | }; 240 | 241 | /** 242 | * Simple conversion method, for transforming bytes in to megabytes 243 | * 244 | * @returns {Number} 245 | * @api public 246 | */ 247 | 248 | History.bytesToMb = function bytesToMb (bytes) { 249 | return Math.round(bytes/1024/1024); 250 | }; 251 | -------------------------------------------------------------------------------- /lib/cluster.exception.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /*! 4 | * cluster.exception 5 | * Copyright(c) 2011 > Future Arnout Kazemier 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies 11 | */ 12 | 13 | var os = require('os') 14 | , fs = require('fs') 15 | , jade = require('jade') 16 | , utils = require('./format') 17 | , format = utils.format 18 | , timestamp = utils.timestamp 19 | , History = require('./history') 20 | , Email = require('email').Email; 21 | 22 | /** 23 | * Cluster.exception allows you to receive emails of uncaught exceptions in your 24 | * node.js applications. Not only the error is send but also a history of the 25 | * `console` statements, memory, cpu, etc, etc, the whole chabang. This might 26 | * give you a better context on why the error was occuring in the first place. 27 | * 28 | * @param {Object} options The configurable options for the cluster.exceptions plugin. 29 | * @param {String} options.from The `from` address field for the e-mail. 30 | * @param {String} options.template The template that is send in the e-mail, can be `default`, `history` and `basic`. 31 | * @param {Object} options.history Options to configure the history generator 32 | * @returns {Function} the configured plugin for the `cluster.use` method. 33 | * @api public 34 | */ 35 | 36 | exports = module.exports = function exceptions (options) { 37 | options = options || {}; 38 | 39 | var log = {} 40 | , to = [] 41 | , from = options.from || 'cluster@dev.null' 42 | , subject = options.subject || 'Cluster.exception {date}' 43 | , methods = options.methods || ['log','info','warn','error'] 44 | , template = options.template || 'default' 45 | , history; 46 | 47 | if (!options.to) { 48 | throw new Error("Please specify a e-mail address for the cluster.exception plugin"); 49 | } 50 | 51 | Array.prototype.push[Array.isArray(options.to) 52 | ? 'apply' 53 | : 'call' 54 | ](to, options.to); 55 | 56 | /** 57 | * Provides a JSON view of the current environment details such 58 | * as load, memory usage, versioning information and so on. 59 | * 60 | * @param {Error} exception The unchaught exception. 61 | * @param {Cluster} instance A reference to the cluster object. 62 | * 63 | * @returns {Object} details about the exception. 64 | * @api private 65 | */ 66 | 67 | function exception (error, instance) { 68 | var master = instance.master ? instance.master : instance 69 | , details = { 70 | // Details about the current environment of the Node process 71 | // this can be helpfull if you have multiple `cluster` instances 72 | // running and reporting to the same account. 73 | environment: { 74 | root: process.cwd() 75 | , arguments: process.argv 76 | , env: process.env 77 | , gid: process.getgid() 78 | , uid: process.getuid() 79 | , pid: process.pid 80 | , versions: { 81 | node: process.versions.node 82 | , v8: process.versions.v8 83 | , ares: process.versions.ares 84 | , libev: process.versions.ev 85 | } 86 | } 87 | 88 | // Some basic details about the current OS we are running on. 89 | , os: { 90 | platform: process.platform 91 | , type: os.type() 92 | , release: os.release() 93 | , hostname: os.hostname() 94 | } 95 | 96 | // The actual error that occured. 97 | , exception: { 98 | message: error.message 99 | , stack: error.stack 100 | } 101 | 102 | // Details about the cluster instance. 103 | , cluster: { 104 | child: instance.isChild 105 | , worker: instance.isWorker 106 | , master: instance.isMaster 107 | , state: master.state 108 | , masterPID: process.env.CLUSTER_MASTER_PID || process.env.CLUSTER_PARENT_PID 109 | , env: instance.env 110 | , startup: instance.startup 111 | } 112 | 113 | // When did the exception occure. 114 | , date: timestamp() 115 | }; 116 | 117 | // if we have log interception enabled, we are going 118 | // to add these to the details aswel. 119 | if (methods.length) { 120 | details.log = {}; 121 | methods.forEach(function addLogs (type) { 122 | details.log[type] = log[type].join('\n'); 123 | }); 124 | } 125 | 126 | // add the graphs. 127 | if (history && history.keys.length) { 128 | details.graph = {}; 129 | history.keys.forEach(function addGraphs(key){ 130 | var graphs = history.toGraph(key) 131 | , graph; 132 | 133 | for(graph in graphs) { 134 | details.graph[graph] = graphs[graph]; 135 | } 136 | }); 137 | } 138 | 139 | // add more cluster details 140 | if (master._killed) 141 | details.cluster.killed = master_killed; 142 | 143 | if (master.children && master.children.length) 144 | details.cluster.workers = master.children.length; 145 | 146 | if (process.env.CLUSTER_WORKER) 147 | details.cluster.worker_id = process.env.CLUSTER_WORKER; 148 | 149 | if (process.env.CLUSTER_REPLACEMENT_MASTER) 150 | details.cluster['master replacement'] = true; 151 | 152 | return details; 153 | } 154 | 155 | /** 156 | * Simple string based replaces / template system. 157 | * 158 | * @param {String} string The template. 159 | * @param {Object} data The data that is used to replace variables from the string. 160 | * 161 | * @returns {String} result 162 | * @api private 163 | */ 164 | 165 | function replace (string, data) { 166 | for (var param in data) { 167 | string = string.replace(new RegExp('{'+ param +'}','g'), data[param]); 168 | } 169 | 170 | return string; 171 | } 172 | 173 | /** 174 | * Override the console object and capture the messages so we 175 | * can send them together with a potential error report to provide 176 | * more context for the error. 177 | */ 178 | 179 | methods.forEach(function replaceConsole (type) { 180 | var original = global.console[type]; 181 | log[type] = []; 182 | 183 | global.console[type] = function () { 184 | log[type].push(timestamp() + ' - ' +format.apply(this, arguments)); 185 | 186 | // remove items from the log 187 | if (log[type].length > 25) 188 | log[type].shift(); 189 | 190 | // make sure the original functionality still works 191 | original.apply(console, arguments); 192 | }; 193 | }); 194 | 195 | /** 196 | * Pre-compile the email template. 197 | */ 198 | 199 | template = jade.compile( 200 | fs.readFileSync( 201 | require('path').join( 202 | __dirname 203 | , '../views/' + template + '.jade' 204 | ) 205 | , 'utf-8' 206 | ) 207 | ); 208 | 209 | /** 210 | * The actual plugin. 211 | * 212 | * @param {Cluster} instance A worker or client instance. 213 | * @api public 214 | */ 215 | 216 | function plugin (instance) { 217 | 218 | // Start tracking the process history 219 | history = new History(options.history || false); 220 | 221 | // Add a listener to the process 222 | process.on('uncaughtException', function captureException (error) { 223 | var details = exception(error, instance) 224 | , message = { 225 | subject: replace(subject, details) 226 | , from: from 227 | , to: to 228 | , bodyType: 'html' 229 | , body: template(details) 230 | }; 231 | 232 | // send the e-mail 233 | var sendmail = new Email(message); 234 | sendmail.send(function (err) { 235 | if (err){ 236 | console.error(err.message); 237 | console.log(timestamp() + 'Failed to send cluster.exception mail, outputting details to stdout:'); 238 | console.dir(details); 239 | } else { 240 | console.log(timestamp() + ' Great success! Cluster.exception mail send.'); 241 | } 242 | }); 243 | 244 | // mimic the default uncaught exception handling for workers 245 | // see https://github.com/LearnBoost/cluster/blob/master/lib/worker.js#L95 246 | if (instance.isWorker) { 247 | // stderr for logs 248 | console.error(error.stack || error.message); 249 | 250 | // report exception 251 | if (instance.master) { 252 | // cluster with server 253 | instance.master.call('workerException', error); 254 | } else { 255 | // cluster without server 256 | instance.call('workerException', error); 257 | } 258 | 259 | // exit 260 | process.nextTick(function (){ 261 | instance.destroy(); 262 | }); 263 | } 264 | }); 265 | } 266 | 267 | // Make sure that we also have it called inside the workers 268 | // because we want to gather addional data. 269 | plugin.enableInWorker = true; 270 | return plugin; 271 | }; 272 | 273 | /** 274 | * Library version. 275 | */ 276 | 277 | exports.version = '0.0.3'; 278 | --------------------------------------------------------------------------------