├── .gitignore ├── .npmignore ├── .travis.yml ├── History.md ├── LICENSE ├── README.md ├── config ├── default.js ├── doc │ ├── README │ ├── assets │ │ ├── css │ │ │ ├── combo-min.css │ │ │ ├── cssgrids-min.css │ │ │ ├── main.css │ │ │ └── sprite.png │ │ └── js │ │ │ ├── combo-min.js │ │ │ └── yui-min.js │ ├── layouts │ │ └── main.handlebars │ └── partials │ │ ├── events.handlebars │ │ ├── files.handlebars │ │ ├── index.handlebars │ │ ├── method.handlebars │ │ └── props.handlebars ├── external.js ├── runtime.json └── test.js ├── dist ├── monitor-all.js ├── monitor-all.min.js ├── monitor.js └── monitor.min.js ├── grunt.js ├── lib ├── Connection.js ├── Log.js ├── Monitor.js ├── Probe.js ├── Router.js ├── Server.js ├── Stat.js ├── Sync.js ├── index.js └── probes │ ├── DataModelProbe.js │ ├── FileProbe.js │ ├── FileSyncProbe.js │ ├── InspectProbe.js │ ├── LogProbe.js │ ├── PollingProbe.js │ ├── ProcessProbe.js │ ├── RecipeProbe.js │ ├── ReplProbe.js │ ├── StatProbe.js │ ├── StreamProbe.js │ └── SyncProbe.js ├── monitor.js ├── package.json ├── test ├── ConnectionTest.js ├── DataModelProbeTest.js ├── FileProbeTest.js ├── InspectTest.js ├── LogTest.js ├── MonitorTest.js ├── ProbeTest.js ├── RecipeProbeTest.js ├── RouterTest.js ├── ServerTest.js ├── StatTest.js └── SyncProbeTest.js └── yuidoc.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /config/doc 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.8" 5 | # - "0.6" // problems w/travis downloading dependencies 6 | before_script: 7 | - npm install -g grunt@0.3.17 8 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.6.10 / 2014-05-16 2 | =================== 3 | 4 | * Added cron/interval polling to RecipeProbe 5 | 6 | 0.6.9 / 2014-03-26 7 | =================== 8 | 9 | * Introduced writable probe attributes via monitor.set() 10 | * Introduced the Recipe probe for monitor automation 11 | * Introduced the DataModel probe for remote state management 12 | * Better Inspect probe evaluation error logging 13 | * Tests for the above 14 | 15 | 0.6.8 / 2014-02-22 16 | =================== 17 | 18 | * Better auto-start sequencing 19 | 20 | 0.6.7 / 2014-02-22 21 | =================== 22 | 23 | * Disable monitor connection until all auto-start monitors are connected 24 | 25 | 0.6.6 / 2014-02-21 26 | =================== 27 | 28 | * Named monitor instances 29 | * Auto-start monitors 30 | * Added tests for server start/stop 31 | * Made the example a little less confusing 32 | 33 | 0.6.5 / 2014-01-06 34 | =================== 35 | 36 | * Updated copyright year 37 | 38 | 0.6.4 / 2013-12-23 39 | =================== 40 | 41 | * Better README 42 | 43 | 0.6.3 / 2013-11-14 44 | =================== 45 | 46 | * Silence error logging that were common-case vs. errors 47 | 48 | 0.6.2 / 2013-11-12 49 | =================== 50 | 51 | * Better console log formatting 52 | * Exported global.Monitor for multi-module loading 53 | 54 | 0.6.1 / 2013-11-07 55 | =================== 56 | 57 | * Bumped up node-config version 58 | 59 | 0.6.0 / 2013-11-06 60 | =================== 61 | 62 | * Project name change from monitor-min 63 | 64 | 0.5.12 / 2013-10-23 65 | =================== 66 | 67 | * Updated config dependency to not freak out if the config directory is absent 68 | 69 | 0.5.11 / 2013-10-22 70 | =================== 71 | 72 | * Added TRACE to default console logging output 73 | * Better server listen error handling and debug output 74 | * Better error message on probe failure 75 | 76 | 0.5.10 / 2013-10-15 77 | =================== 78 | 79 | * Better console logging 80 | * Can chain require().start() 81 | * Travis test 82 | 83 | 0.5.9 / 2013-10-04 84 | ================== 85 | 86 | * Better probe instantiation error message 87 | 88 | 0.5.8 / 2013-09-15 89 | ================== 90 | 91 | * Use a gateway if available 92 | * More efficient localhost processing 93 | * Don't restart if already started 94 | * Fixed lint issues 95 | 96 | 0.5.7 / 2013-08-28 97 | ================== 98 | 99 | * Allow socket.io to connect any way it can 100 | * Fixed process.uptime 101 | * Detect and prevent stat & log recursion 102 | * Improved log usage 103 | 104 | 0.5.6 / 2013-07-22 105 | ================== 106 | 107 | * Changed log/stat probe timestamp from integer to ISO 108 | 109 | 0.5.6 / 2013-07-19 110 | ================== 111 | 112 | * Added a timestamp to logProbe/statProbe bundles 113 | 114 | 0.5.5 / 2013-07-18 115 | ================== 116 | 117 | * Added Log & Stat classes 118 | * Tests for Stat & Log 119 | * Added LogProbe & StatProbe 120 | * Added Log.console for console logging 121 | * Added log statements & stats gathering 122 | 123 | 0.5.4 / 2013-05-23 124 | ================== 125 | 126 | * Exported documentation to a public site 127 | * Updated links in README.txt 128 | 129 | 0.5.3 / 2013-05-16 130 | ================== 131 | 132 | * Fixed a dependency issue in the monitor.js bootstrap 133 | 134 | 0.5.2 / 2013-04-17 135 | ================== 136 | 137 | * Revved the major version to match node-monitor major version 138 | * Added Stats and Logs 139 | 140 | 0.1.1 / 2013-03-21 141 | ================== 142 | 143 | * Initial checkin after montior re-org 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2014 Loren West and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Monitor your Node.js application 2 | ================================ 3 | 4 | [![Build Status](https://secure.travis-ci.org/lorenwest/node-monitor.png?branch=master)](https://travis-ci.org/lorenwest/node-monitor) 5 | 6 | Introduction 7 | ------------ 8 | 9 | Node-monitor is a library for remote monitoring and control of your Node.js app servers. 10 | 11 | Like JMX in the Java world, node-monitor comes with a handful of general monitors, and allows you to create custom monitors for your application. 12 | 13 | These monitors can be scripted using JavaScript, or placed onto a dashboard. 14 | 15 | Project Guidelines 16 | ------------------ 17 | 18 | * *Simple* - Get started quickly 19 | * *Powerful* - For multi-node enterprise deployment 20 | * *Lightweight* - Inactive until used, small footprint during use 21 | * *Flexible* - Easy to write custom monitors for your app 22 | * *Stable* - Well tested foundation for module developers 23 | 24 | 25 | Getting Started 26 | --------------- 27 | 28 | Run the following from your app server directory 29 | 30 | $ npm install monitor 31 | 32 | Then place the following line in your application bootstrap, and restart your server 33 | 34 | require('monitor').start(); 35 | 36 | Monitoring your app with a REPL console 37 | --------------------------------------- 38 | 39 | Ad-hoc monitoring can be done from a REPL console. 40 | 41 | Start up the REPL, and get the Monitor class. Feel free to copy/paste these lines into your console: 42 | 43 | $ node 44 | > var Monitor = require('monitor'); 45 | undefined 46 | 47 | Now connect a monitor to a probe on your app server. There are a handful of built-in probes, and you can build custom probes for your application or npm module. 48 | 49 | For this example, we'll monitor the *Process* probe: 50 | 51 | > var processMonitor = new Monitor({probeClass:'Process'}); 52 | > processMonitor.connect(); 53 | 54 | The monitor is a [Backbone.js](http://backbonejs.org/) data model so it updates in real time, and you can get all fields with toJSON(): 55 | 56 | > processMonitor.get('freemem'); 57 | 86368256 58 | > processMonitor.get('freemem'); 59 | 80044032 60 | > processMonitor.toJSON(); 61 | ... 62 | 63 | As the monitor changes, it emits change events: 64 | 65 | > processMonitor.on('change', function() { 66 | ... console.log(processMonitor.get('freemem')); 67 | ... }); 68 | 69 | Monitoring your app with a custom script 70 | ---------------------------------------- 71 | 72 | Using Node.js as a scripting language, you can write custom monitors that do anything Node.js can do. Here's an example that prints to the console when free memory falls below a threshold. 73 | 74 | Save this file to low-memory-warn.js, and run **node low-memory-warn** 75 | 76 | // Low memory warning monitor 77 | var Monitor = require('monitor'); 78 | var LOW_MEMORY_THRESHOLD = 100000000; 79 | 80 | // Set the probe to push changes every 10 seconds 81 | var options = { 82 | hostName: 'localhost', 83 | probeClass: 'Process', 84 | initParams: { 85 | pollInterval: 10000 86 | } 87 | } 88 | var processMonitor = new Monitor(options); 89 | 90 | // Attach the change listener 91 | processMonitor.on('change', function() { 92 | var freemem = processMonitor.get('freemem'); 93 | if (freemem < LOW_MEMORY_THRESHOLD) { 94 | console.log('Low memory warning: ' + freemem); 95 | } 96 | }); 97 | 98 | // Now connect the monitor 99 | processMonitor.connect(function(error) { 100 | if (error) { 101 | console.error('Error connecting with the process probe: ', error); 102 | process.exit(1); 103 | } 104 | }); 105 | 106 | Monitoring your app in a browser 107 | -------------------------------- 108 | 109 | The above script runs just as well within an html ``` 112 | 113 | The browser distribution included in node-monitor exports a single variable ```Monitor``` to the global namespace, and it can be used just like the ```Monitor``` variable in ```var Monitor = require('monitor')```. 114 | 115 | Your browser will probably have to be pointing to localhost or behind your firewall in order to connect with the app server on the configured monitor port. See *Security Concerns* below. 116 | 117 | Monitoring your app in a dashboard 118 | --------------------------- 119 | ![Monitor-Dashboard](http://lorenwest.github.io/monitor-dashboard/img/cpu-gauge.png) 120 | 121 | The [monitor-dashboard](https://github.com/lorenwest/monitor-dashboard) application lets you visualize your monitors in a dashboard. 122 | 123 | $ npm install monitor-dashboard 124 | $ npm start monitor-dashboard 125 | 126 | Security Concerns 127 | ----------------- 128 | 129 | Exposing the internals of your app server is a high security risk. By default, the server listens on port 42000 and will connect with localhost clients only. 130 | 131 | In order to monitor across machines, the default configuration must be changed to listen beyond localhost. Before doing this, it is recommended to understand the risks and have external measures in place to prevent unauthorized access. 132 | 133 | See notes in the ```config/external.js``` file for more information. 134 | 135 | Links 136 | ------- 137 | 138 | * [API Docs](http://lorenwest.github.io/node-monitor/doc/index.html) - Node monitor JavaScript documentation. 139 | * [Monitor Dashboard](https://github.com/lorenwest/monitor-dashboard) - Dashboards for the node monitor project. 140 | * [Dashboard Components](https://github.com/lorenwest/core-monitor) - Core components for the Dashboard project. 141 | 142 | License 143 | ------- 144 | 145 | May be freely distributed under the MIT license
146 | See the [LICENSE](https://github.com/lorenwest/node-monitor/blob/master/LICENSE) file.
147 | Copyright (c) 2010-2014 Loren West
148 | 149 | -------------------------------------------------------------------------------- /config/default.js: -------------------------------------------------------------------------------- 1 | // Default configurations. 2 | module.exports = { 3 | Monitor: { 4 | 5 | // This is the running applicaiton name. It should be overridden 6 | // in applications that embed the monitor package. 7 | appName: 'Monitor', 8 | 9 | // The base port to use for monitor connections. If this is changed, 10 | // it must be changed on all processes in the monitor network as it 11 | // is used by both client and server processes. Clients use this as 12 | // the starting port to scan. Servers attempt to listen on this port, 13 | // and will continue with higher ports if other processes are listening 14 | // on the port. 15 | serviceBasePort: 42000, 16 | 17 | // When attempting to connect to a remote server, scan this number of 18 | // ports on the remote machine (starting at the serviceBasePort) to 19 | // discover monitor processes. 20 | portsToScan: 20, 21 | 22 | // Only allow connections from this machine by default. This reduces 23 | // accidental security breaches by requiring you to consider your network 24 | // security policies before allowing external connections. 25 | // See the external.js file in this directory for more information. 26 | allowExternalConnections: false, 27 | 28 | // Monitors to start on load. 29 | // This is a map of objects, each passed as the first parameter 30 | // to a new Monitor instance. The autoStart monitors are named so 31 | // additional autoStart monitors can be added in subsequent config files. 32 | autoStart: { 33 | // monitorName: {probeName: '...', probeClass: '...', initParams:{...}} 34 | }, 35 | 36 | // Named list of recipe definitions to load when Monitor loads. 37 | // See Recipe.js for the structure and behavior of a recipe. 38 | recipes: { 39 | // recipeName: {recipeDefinition} 40 | }, 41 | 42 | // Configure the built-in console log output 43 | consoleLogListener: { 44 | pattern: "{trace,warn,error,fatal}.*" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/doc/README: -------------------------------------------------------------------------------- 1 | These override from the default yuidoc templates installed globally: 2 | 3 | /usr/local/lib/node_modules/yuidocjs/themes/default 4 | 5 | In order to load locally vs. requiring a network connection (required for developing on the train): 6 | 7 | * Download yui-min.js into assets/js/yui-min.js 8 | * Remove the reference to http://yui.yahooapis.com/ 9 | * Download all combo* .js files that it grabs from yahoo -> into assets/js/combo-min.js 10 | * Download all combo* .css files -> into assets/css/combo-min.css 11 | * Download sprite.png -> into assets/css/sprite.png 12 | * Change all sprite.png references from url(http...sprite.png) to url(sprite.png) in combo-min.css 13 | * Edit the main.handlebars to include assets/js/combo-min.js and assets/css/combo-min.css 14 | 15 | -------------------------------------------------------------------------------- /config/doc/assets/css/combo-min.css: -------------------------------------------------------------------------------- 1 | /* 2 | YUI 3.4.0 (build 3928) 3 | Copyright 2011 Yahoo! Inc. All rights reserved. 4 | Licensed under the BSD License. 5 | http://yuilibrary.com/license/ 6 | */ 7 | .yui3-widget-hidden{display:none}.yui3-widget-content{overflow:hidden}.yui3-widget-content-expanded{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;height:100%}.yui3-widget-tmp-forcesize{overflow:hidden!important} 8 | /* 9 | YUI 3.4.0 (build 3928) 10 | Copyright 2011 Yahoo! Inc. All rights reserved. 11 | Licensed under the BSD License. 12 | http://yuilibrary.com/license/ 13 | */ 14 | .yui3-tab-panel{display:none}.yui3-tab-panel-selected{display:block}.yui3-tabview-list,.yui3-tab{margin:0;padding:0;list-style:none}.yui3-tabview{position:relative}.yui3-tabview,.yui3-tabview-list,.yui3-tabview-panel,.yui3-tab,.yui3-tab-panel{zoom:1}.yui3-tab{display:inline-block;*display:inline;vertical-align:bottom;cursor:pointer}.yui3-tab-label{display:block;display:inline-block;padding:6px 10px;position:relative;text-decoration:none;vertical-align:bottom}.yui3-skin-sam .yui3-tabview-list{border:solid #2647a0;border-width:0 0 5px;zoom:1}.yui3-skin-sam .yui3-tab{margin:0 .2em 0 0;padding:1px 0 0;zoom:1}.yui3-skin-sam .yui3-tab-selected{margin-bottom:-1px}.yui3-skin-sam .yui3-tab-label{background:#d8d8d8 url(sprite.png) repeat-x;border:solid #a3a3a3;border-width:1px 1px 0 1px;color:#000;cursor:hand;font-size:85%;padding:.3em .75em;text-decoration:none}.yui3-skin-sam .yui3-tab-label:hover,.yui3-skin-sam .yui3-tab-label:focus{background:#bfdaff url(sprite.png) repeat-x left -1300px;outline:0}.yui3-skin-sam .yui3-tab-selected .yui3-tab-label,.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:focus,.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:hover{background:#2647a0 url(sprite.png) repeat-x left -1400px;color:#fff}.yui3-skin-sam .yui3-tab-selected .yui3-tab-label{padding:.4em .75em}.yui3-skin-sam .yui3-tab-selected .yui3-tab-label{border-color:#243356}.yui3-skin-sam .yui3-tabview-panel{background:#edf5ff}.yui3-skin-sam .yui3-tabview-panel{border:1px solid #808080;border-top-color:#243356;padding:.25em .5em} 15 | -------------------------------------------------------------------------------- /config/doc/assets/css/cssgrids-min.css: -------------------------------------------------------------------------------- 1 | /* 2 | YUI 3.4.0 (build 3928) 3 | Copyright 2011 Yahoo! Inc. All rights reserved. 4 | Licensed under the BSD License. 5 | http://yuilibrary.com/license/ 6 | */ 7 | .yui3-g{letter-spacing:-0.31em;*letter-spacing:normal;word-spacing:-0.43em}.yui3-u,.yui3-u-1,.yui3-u-1-2,.yui3-u-1-3,.yui3-u-2-3,.yui3-u-1-4,.yui3-u-3-4,.yui3-u-1-5,.yui3-u-2-5,.yui3-u-3-5,.yui3-u-4-5,.yui3-u-1-6,.yui3-u-5-6,.yui3-u-1-8,.yui3-u-3-8,.yui3-u-5-8,.yui3-u-7-8,.yui3-u-1-12,.yui3-u-5-12,.yui3-u-7-12,.yui3-u-11-12,.yui3-u-1-24,.yui3-u-5-24,.yui3-u-7-24,.yui3-u-11-24,.yui3-u-13-24,.yui3-u-17-24,.yui3-u-19-24,.yui3-u-23-24{display:inline-block;zoom:1;*display:inline;letter-spacing:normal;word-spacing:normal;vertical-align:top}.yui3-u-1{display:block}.yui3-u-1-2{width:50%}.yui3-u-1-3{width:33.33333%}.yui3-u-2-3{width:66.66666%}.yui3-u-1-4{width:25%}.yui3-u-3-4{width:75%}.yui3-u-1-5{width:20%}.yui3-u-2-5{width:40%}.yui3-u-3-5{width:60%}.yui3-u-4-5{width:80%}.yui3-u-1-6{width:16.656%}.yui3-u-5-6{width:83.33%}.yui3-u-1-8{width:12.5%}.yui3-u-3-8{width:37.5%}.yui3-u-5-8{width:62.5%}.yui3-u-7-8{width:87.5%}.yui3-u-1-12{width:8.3333%}.yui3-u-5-12{width:41.6666%}.yui3-u-7-12{width:58.3333%}.yui3-u-11-12{width:91.6666%}.yui3-u-1-24{width:4.1666%}.yui3-u-5-24{width:20.8333%}.yui3-u-7-24{width:29.1666%}.yui3-u-11-24{width:45.8333%}.yui3-u-13-24{width:54.1666%}.yui3-u-17-24{width:70.8333%}.yui3-u-19-24{width:79.1666%}.yui3-u-23-24{width:95.8333%} -------------------------------------------------------------------------------- /config/doc/assets/css/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenwest/node-monitor/7afd5d22c1d44b93c7cb2250ea9e05ef7b8451f8/config/doc/assets/css/sprite.png -------------------------------------------------------------------------------- /config/doc/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{htmlTitle}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |

{{projectDescription}} v{{projectVersion}}

19 |
20 |
21 |
22 | 23 |
24 | 27 |
28 |
29 | {{>options}} 30 |
31 |
32 |
33 | {{>layout_content}} 34 |
35 |
36 |
37 |
38 |
39 |
40 | 43 | 46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /config/doc/partials/events.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{name}} 4 |

5 | {{#crossLink type}}{{/crossLink}} 6 | 7 | {{#if deprecated}} 8 | deprecated 9 | {{/if}} 10 | 11 | {{#if access}} 12 | {{access}} 13 | {{/if}} 14 | 15 | {{#if final}} 16 | final 17 | {{/if}} 18 | 19 | {{#if static}} 20 | static 21 | {{/if}} 22 | 23 |
24 | {{#if overwritten_from}} 25 |

Inherited from 26 | 27 | {{overwritten_from/class}} 28 | 29 | {{#if foundAt}} 30 | but overwritten in 31 | {{/if}} 32 | {{else}} 33 | {{#if extended_from}} 34 |

Inherited from 35 | {{extended_from}}: 36 | {{else}} 37 | {{#providedBy}} 38 |

Provided by the {{.}} module.

39 | {{/providedBy}} 40 |

41 | {{/if}} 42 | {{/if}} 43 |

44 | 45 | {{#if deprecationMessage}} 46 |

Deprecated: {{deprecationMessage}}

47 | {{/if}} 48 | 49 | {{#if since}} 50 |

Available since {{since}}

51 | {{/if}} 52 |
53 | 54 |
55 | {{{eventDescription}}} 56 |
57 | 58 | {{#if params}} 59 |
60 |

Event Payload:

61 | 62 | 114 |
115 | {{/if}} 116 | 117 | 118 | {{#example}} 119 |
120 |

Example:

121 | 122 |
123 | {{{.}}} 124 |
125 |
126 | {{/example}} 127 |
128 | -------------------------------------------------------------------------------- /config/doc/partials/files.handlebars: -------------------------------------------------------------------------------- 1 |

File: {{fileName}}

2 | 3 |
4 |
5 | {{fileData}}
6 |     
7 |
8 | -------------------------------------------------------------------------------- /config/doc/partials/index.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Node.js Application Monitoring

5 |

Remote monitoring and control for your Node.js app

6 | 7 |

Monitor Core

8 |

9 | Classes in this module represent baseline monitor functionality. They can 10 | be loaded and run in a node.js container as well as within a browser. 11 |

12 | 13 |

Baseline Probes

14 |

15 | The probes in this module offer baseline functionality, and provide examples 16 | for building custom probes. 17 |

18 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /config/doc/partials/method.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{name}} 4 |

5 | 6 | {{#if params}} 7 |
8 | () 19 |
20 | {{else}} 21 | () 22 | {{/if}} 23 | 24 | {{#if return}} 25 | 26 | {{#crossLink returnType}}{{/crossLink}} 27 | 28 | {{/if}} 29 | 30 | {{#if deprecated}} 31 | deprecated 32 | {{/if}} 33 | 34 | {{#if access}} 35 | {{access}} 36 | {{/if}} 37 | 38 | {{#if final}} 39 | final 40 | {{/if}} 41 | 42 | {{#if static}} 43 | static 44 | {{/if}} 45 | 46 | {{#if chainable}} 47 | chainable 48 | {{/if}} 49 | 50 |
51 | {{#if overwritten_from}} 52 |

Inherited from 53 | 54 | {{overwritten_from/class}} 55 | 56 | {{#if foundAt}} 57 | but overwritten in 58 | {{/if}} 59 | {{else}} 60 | {{#if extended_from}} 61 |

Inherited from 62 | {{extended_from}}: 63 | {{else}} 64 | {{#providedBy}} 65 |

Provided by the {{.}} module.

66 | {{/providedBy}} 67 |

68 | {{/if}} 69 | {{/if}} 70 |

71 | 72 | 73 | {{#if deprecationMessage}} 74 |

Deprecated: {{deprecationMessage}}

75 | {{/if}} 76 | 77 | {{#if since}} 78 |

Available since {{since}}

79 | {{/if}} 80 |
81 | 82 |
83 | {{{methodDescription}}} 84 |
85 | 86 | {{#if params}} 87 |
88 |

Parameters:

89 | 90 | 154 |
155 | {{/if}} 156 | 157 | {{#return}} 158 |
159 |

Returns:

160 | 161 |
162 | {{#if description}} 163 | {{#if type}} 164 | {{#crossLink type}}{{/crossLink}}: 165 | {{/if}} 166 | {{{description}}} 167 | {{else}} 168 | {{#if type}} 169 | {{#crossLink type}}{{/crossLink}}: 170 | {{/if}} 171 | {{/if}} 172 |
173 |
174 | {{/return}} 175 | 176 | {{#example}} 177 |
178 |

Example:

179 | 180 |
181 | {{{.}}} 182 |
183 |
184 | {{/example}} 185 |
186 | -------------------------------------------------------------------------------- /config/doc/partials/props.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{name}} 4 |

5 | {{#crossLink type}}{{/crossLink}} 6 | 7 | {{#if deprecated}} 8 | deprecated 9 | {{/if}} 10 | 11 | {{#if access}} 12 | {{access}} 13 | {{/if}} 14 | 15 | {{#if final}} 16 | final 17 | {{/if}} 18 | 19 | {{#if static}} 20 | static 21 | {{/if}} 22 | 23 |
24 | {{#if overwritten_from}} 25 |

Inherited from 26 | 27 | {{overwritten_from/class}} 28 | 29 | {{#if foundAt}} 30 | but overwritten in 31 | {{/if}} 32 | {{else}} 33 | {{#if extended_from}} 34 |

Inherited from 35 | {{extended_from}}: 36 | {{else}} 37 | {{#providedBy}} 38 |

Provided by the {{.}} module.

39 | {{/providedBy}} 40 |

41 | {{/if}} 42 | {{/if}} 43 |

44 | 45 | {{#if deprecationMessage}} 46 |

Deprecated: {{deprecationMessage}}

47 | {{/if}} 48 | 49 | {{#if since}} 50 |

Available since {{since}}

51 | {{/if}} 52 |
53 | 54 |
55 | {{{propertyDescription}}} 56 |
57 | 58 | {{#if default}} 59 |

Default: {{default}}

60 | {{/if}} 61 | 62 | {{#example}} 63 |
64 |

Example:

65 | 66 |
67 | {{{.}}} 68 |
69 |
70 | {{/example}} 71 | 72 | {{#if subprops}} 73 |

Sub-properties:

74 | 75 | 114 | {{/if}} 115 |
116 | -------------------------------------------------------------------------------- /config/external.js: -------------------------------------------------------------------------------- 1 | // Configuration overrides when NODE_ENV=external 2 | 3 | // This configuration allows incoming connections from remote systems. 4 | // It should be used only after assuring the network firewall prevents 5 | // untrusted connections on the service port range (usually 42000+). 6 | module.exports = { 7 | Monitor: { 8 | allowExternalConnections: true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config/runtime.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /config/test.js: -------------------------------------------------------------------------------- 1 | // Configurations for running tests 2 | var NL = '\n'; 3 | module.exports = { 4 | Monitor: { 5 | autoStart: { 6 | testProbe: { 7 | probeName: 'ProcessTest', probeClass: 'Process', initParams:{pollInterval: 1234} 8 | }, 9 | testModel: { 10 | probeName: 'DataModelTest', probeClass: 'DataModel', initParams:{ 11 | testParam1:'testValue1', 12 | attr1:'attrValue1', 13 | derivedAttr1: 'derivedAttrValue1' 14 | } 15 | }, 16 | testRecipe: { 17 | probeName: 'RecipeTest', probeClass: 'Recipe', initParams:{ 18 | autoStart:true, 19 | monitors: { 20 | process: {probeName: 'ProcessTest'}, 21 | dataModel: {probeName: 'DataModelTest'} 22 | }, 23 | script: 24 | "// Forward attr1 to attr2, showing the script was run" + NL + 25 | " dataModel.set('attr2', dataModel.get('attr1'));" 26 | } 27 | } 28 | }, 29 | 30 | // Squelch log output so error tests aren't chatty 31 | consoleLogListener: { 32 | pattern_save1: "{debug,warn,error,fatal}.*", 33 | pattern_save2: "*", 34 | pattern: "" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /grunt.js: -------------------------------------------------------------------------------- 1 | // grunt.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For all details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | 6 | var exec = require('child_process').exec; 7 | 8 | // This is used in the build automation tasks, and on the server 9 | // when running in dev mode to serve individual files for debugging. 10 | var MODULE_DEF = { 11 | lib: [ 12 | "lib/Monitor.js", 13 | "lib/Stat.js", 14 | "lib/Log.js", 15 | "lib/Probe.js", 16 | "lib/Connection.js", 17 | "lib/Server.js", 18 | "lib/Router.js", 19 | "lib/Sync.js", 20 | "lib/probes/DataModelProbe.js", 21 | "lib/probes/RecipeProbe.js", 22 | "lib/probes/PollingProbe.js", 23 | "lib/probes/StreamProbe.js", 24 | "lib/probes/InspectProbe.js", 25 | "lib/probes/StatProbe.js", 26 | "lib/probes/LogProbe.js" 27 | ], 28 | ext: [ 29 | "node_modules/underscore/underscore.js", 30 | "node_modules/backbone/backbone.js", 31 | "node_modules/backbone-callbacks/backbone-callbacks.js", 32 | "node_modules/socket.io-client/dist/socket.io.js" 33 | ], 34 | probes: [ 35 | "lib/probes/FileProbe.js", 36 | "lib/probes/ReplProbe.js", 37 | "lib/probes/ProcessProbe.js", 38 | "lib/probes/SyncProbe.js", 39 | "lib/probes/FileSyncProbe.js" 40 | ] 41 | }; 42 | 43 | // Build automation tasks 44 | module.exports = function(grunt) { 45 | 46 | // Project configuration. 47 | grunt.initConfig({ 48 | pkg: '', 49 | monitor: MODULE_DEF, 50 | meta: { 51 | banner: '/* <%= pkg.name %> - v<%= pkg.version %> - ' + 52 | '<%= grunt.template.today("yyyy-mm-dd") %> */' 53 | }, 54 | lint: { 55 | files: ['grunt.js', '', '', 'test/*.js'] 56 | }, 57 | test: { 58 | files: ['test/*.js'] 59 | }, 60 | watch: { 61 | files: ['grunt.js', 'yuidoc.json', '', '', 'config/doc/**', 'test/*.js'], 62 | tasks: 'doc lint test' 63 | }, 64 | concat: { 65 | lib: { 66 | src: ['', ''], 67 | dest: './dist/monitor.js' 68 | }, 69 | all: { 70 | src: ['', '', ''], 71 | dest: './dist/monitor-all.js' 72 | } 73 | }, 74 | min: { 75 | lib: { 76 | src: ['', './dist/monitor.js'], 77 | dest: './dist/monitor.min.js' 78 | 79 | }, 80 | all: { 81 | src: ['', './dist/monitor-all.js'], 82 | dest: './dist/monitor-all.min.js' 83 | } 84 | }, 85 | jshint: { 86 | options: { 87 | strict: false, 88 | curly: true, 89 | eqeqeq: true, 90 | immed: true, 91 | latedef: true, 92 | newcap: true, 93 | noarg: true, 94 | sub: true, 95 | undef: true, 96 | boss: true, 97 | eqnull: true, 98 | node: true 99 | }, 100 | globals: { 101 | exports: true 102 | } 103 | } 104 | }); 105 | 106 | grunt.registerTask('doc', 'Generate documentation files', function() { 107 | var t = this, done = t.async(), child, version = grunt.config.get('pkg').version; 108 | var cmd = 'yuidoc --project-version ' + version; 109 | console.log(cmd); 110 | child = exec(cmd, function (error, stdout, stderr) { 111 | console.log(stderr); 112 | console.log(stdout); 113 | cmd = 'cp -R doc/* ../node-monitor-pages/doc; rm -rf doc'; 114 | console.log(cmd); 115 | child = exec(cmd, function (error, stdout, stderr) { 116 | console.log(stderr); 117 | console.log(stdout); 118 | done(); 119 | }); 120 | }); 121 | }); 122 | 123 | grunt.registerTask('rm_dist', 'Remove distribution files', function() { 124 | var t = this, done = t.async(), child; 125 | child = exec('rm -f dist/*', function (error, stdout, stderr) { 126 | console.log(stderr); 127 | console.log(stdout); 128 | done(); 129 | }); 130 | }); 131 | 132 | // Default task. 133 | grunt.registerTask('default', 'doc lint test dist'); 134 | grunt.registerTask('dist', 'rm_dist concat:lib concat:all min:lib min:all'); 135 | 136 | }; 137 | 138 | // Expose externally 139 | module.exports.MODULE_DEF = MODULE_DEF; 140 | -------------------------------------------------------------------------------- /lib/Log.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true */ 2 | // Log.js (c) 2010-2014 Loren West and other contributors 3 | // May be freely distributed under the MIT license. 4 | // For further details and documentation: 5 | // http://lorenwest.github.com/node-monitor 6 | (function(root){ 7 | 8 | // Module loading 9 | var Monitor = root.Monitor || require('./Monitor'), 10 | // Raw events on the server (for speed), backbone events on the browser (for functionality) 11 | EventEmitter = Monitor.commonJS ? require('events').EventEmitter.prototype : Monitor.Backbone.Events, 12 | Stat = Monitor.Stat, 13 | stat = new Stat('Log'), 14 | _ = Monitor._, 15 | emittingNow = false; 16 | 17 | /** 18 | * A lightweight component for gathering and emitting application logs 19 | * 20 | * It's designed with low development and runtime cost in mind, encouraging 21 | * usage with minimum concern for overhead. Runtime monitoring can be as chatty 22 | * as desired, outputting every log statement of every type, or finely tuned 23 | * with regular expressions to monitor specific log statements. 24 | * 25 | * Log Collector 26 | * ------------- 27 | * 28 | * As a collector, it's a place to send application logs. 29 | * 30 | * Example for outputting a log in your application: 31 | * 32 | * var log = require('monitor').getLogger('myModule'); 33 | * ... 34 | * log.info('Credit limit accepted', limit, requestedAmount); 35 | * 36 | * The above is a request to output an ```info``` log for ```myModule``` named 37 | * ```Credit limit accepted```. The log entry includes all additional parameters, 38 | * in this case the customer credit limit and the reqeusted amount. 39 | * 40 | * The full name for this log entry is: ```"info.myModule.Credit limit accepted"``` 41 | * The name is important, as monitors can be configured to output logs based 42 | * on this name. 43 | * 44 | * Best practices are to include dynamic parameters in extra arguments 45 | * vs. concatenating strings. This reduces logging overhead, especially 46 | * for log statements that aren't currently being watched. 47 | * 48 | * Log Emitter 49 | * ----------- 50 | * As an emitter, the Log module is a place to capture logging output. 51 | * 52 | * When listening for log entries, wildcards can be used to register for 53 | * particular log types and entries. 54 | * 55 | * var Log = require('monitor').Log; 56 | * ... 57 | * Log.on('info.myModule.*', myFunction); 58 | * 59 | * Will call ```myFunction``` when all ```info.myModule.*``` logs are emitted. 60 | * 61 | * Listeners are invoked with the following arguments: 62 | * 63 | * - type - The log type (trace, debug, info, warn, error, or fatal) 64 | * - module - The logger module name 65 | * - name - The log entry name 66 | * - args... - Additional arguments passed into the log entry are passed on 67 | * as additional args to the event listener. 68 | * 69 | * Wildcards 70 | * --------- 71 | * A flexible and user-oriented wildcard pattern is used for monitoring 72 | * logs. The pattern is described in the Wildcard secttion of the Stats class. 73 | * 74 | * Choosing Good Names 75 | * ------------------- 76 | * It's a good idea to pick a good naming scheme with each dot-delimited segment 77 | * having a consistent, well-defined purpose. Volatile segments should be as deep 78 | * into the hierarchy (furthest right) as possible. Keeping the names less 79 | * volatile makes it easier to turn statistics recording on for all logs. 80 | * 81 | * @class Log 82 | * @constructor 83 | */ 84 | var Log = Monitor.Log = function(module) { 85 | var t = this; 86 | t.module = module; 87 | }; 88 | var proto = Log.prototype; 89 | 90 | // This is a map of registered event names to compiled regexs, for 91 | // quickly testing if a log needs to be emitted. 92 | Log.eventRegex = {}; 93 | 94 | /** 95 | * Output a ```trace``` log entry 96 | * 97 | * @method trace 98 | * @param name {String} Log entry name 99 | * @param [...] {Any} Subsequent arguments to add to the log 100 | */ 101 | 102 | /** 103 | * Output a ```debug``` log entry 104 | * 105 | * @method debug 106 | * @param name {String} Log entry name 107 | * @param [...] {Any} Subsequent arguments to add to the log 108 | */ 109 | 110 | /** 111 | * Output a ```info``` log entry 112 | * 113 | * @method info 114 | * @param name {String} Log entry name 115 | * @param [...] {Any} Subsequent arguments to add to the log 116 | */ 117 | 118 | /** 119 | * Output a ```warn``` log entry 120 | * 121 | * @method warn 122 | * @param name {String} Log entry name 123 | * @param [...] {Any} Subsequent arguments to add to the log 124 | */ 125 | 126 | /** 127 | * Output a ```error``` log entry 128 | * 129 | * @method error 130 | * @param name {String} Log entry name 131 | * @param [...] {Any} Subsequent arguments to add to the log 132 | */ 133 | 134 | /** 135 | * Output a ```fatal``` log entry 136 | * 137 | * @method fatal 138 | * @param name {String} Log entry name 139 | * @param [...] {Any} Subsequent arguments to add to the log 140 | */ 141 | 142 | // Add a method for each log type 143 | ['trace','debug','info','warn','error','fatal'].forEach(function(method) { 144 | proto[method] = function(name) { 145 | Log._emit(method, this.module, name, arguments); 146 | }; 147 | }); 148 | 149 | /** 150 | * Send the log to all registered listeners 151 | * 152 | * @private 153 | * @static 154 | * @method emit 155 | * @param type {string} The log type (trace, debug, info, etc) 156 | * @param module {String} The log module name 157 | * @param name {String} The log entry name 158 | * @param args {any[]} Arguments to the log entry 159 | */ 160 | Log._emit = function(type, module, name, args) { 161 | var eventName, 162 | fullName = type + '.' + module + '.' + name; 163 | 164 | // Prevent log recursion. This has the effect of disabling all logging 165 | // for log handlers (and their downstream effect), but is necessary to 166 | // prevent infinite recursion. If it's desired to log the output of 167 | // log handlers, then delay that processing until nextTick. 168 | if (emittingNow) { 169 | return; 170 | } 171 | emittingNow = true; 172 | 173 | // Output a counter stat for this log 174 | stat.increment(fullName); 175 | 176 | // Test the name against all registered events 177 | for (eventName in Log._events) { 178 | 179 | // Get the regex associated with the name (using the Stat package) 180 | var regex = Log.eventRegex[eventName]; 181 | if (!regex) { 182 | regex = Log.eventRegex[eventName] = Stat._buildRegex(eventName); 183 | } 184 | 185 | // Test the long name with the regex, and emit if it matches 186 | if (regex.test(fullName)) { 187 | 188 | // Build the arguments as event name, log type, module, name, [other args...] 189 | var allArgs = _.toArray(args), 190 | emitFn = Log.emit || Log.trigger; // NodeJS/server=emit, Backbone/browser=trigger 191 | allArgs.splice(0, 1, eventName, type, module, name); 192 | emitFn.apply(Log, allArgs); 193 | } 194 | } 195 | 196 | // Turn off recursion prevention 197 | emittingNow = false; 198 | }; 199 | 200 | // Mixin event processing for the Log class 201 | _.extend(Log, EventEmitter); 202 | 203 | // Expose this class from the Monitor module 204 | Monitor.setLoggerClass(Log); 205 | 206 | /** 207 | * Output log statements to the console 208 | * 209 | * This method can be used as a listener to send logs to the console. 210 | * 211 | * It uses console.error() for error and fatal log types, and console.log() 212 | * for all other log types. 213 | * 214 | * Example: 215 | * 216 | * var Log = Monitor.Log; 217 | * Log.on('*.MyModule.*', Log.console); 218 | * 219 | * @static 220 | * @method consoleLogger 221 | * @param type {string} The log type (trace, debug, info, etc) 222 | * @param module {String} The log module name 223 | * @param name {String} The log entry name 224 | * @param args {any...} All original, starting with the short name 225 | */ 226 | Log.console = function(type, module, name) { 227 | 228 | // Build the string to log, in log4js format 229 | var nowStr = (new Date()).toJSON(), 230 | args = _.toArray(arguments), 231 | logStr = '[' + nowStr + '] [' + type.toUpperCase() + '] ' + module; 232 | 233 | // Remove the type, module, name leaving the args to the log 234 | args.splice(0,3); 235 | 236 | // If no args, then they didn't provide a name 237 | if (args.length === 0) { 238 | args = [name]; 239 | } 240 | else { 241 | // Add the log entry name 242 | logStr += '.' + name; 243 | } 244 | 245 | // If the output is simple, just print it. Otherwise JSON.stringify it. 246 | logStr += ' - '; 247 | if (args.length === 1 && typeof args[0] === 'string') { 248 | logStr += args[0]; 249 | } 250 | else { 251 | try { 252 | logStr += JSON.stringify(args); 253 | } catch(e) { 254 | logStr += Monitor.stringify(args); 255 | } 256 | } 257 | 258 | // Send to the console - Log or error 259 | if (type === 'error' || type === 'fatal') { 260 | console.error(logStr); 261 | } 262 | else { 263 | console.log(logStr); 264 | } 265 | 266 | }; 267 | 268 | // Attach the console log listener 269 | var pattern = Monitor.Config.Monitor.consoleLogListener.pattern; 270 | if (pattern) { 271 | Log.on(pattern, Log.console); 272 | } 273 | 274 | }(this)); 275 | -------------------------------------------------------------------------------- /lib/Probe.js: -------------------------------------------------------------------------------- 1 | // Probe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // Module loading 8 | var Monitor = root.Monitor || require('./Monitor'), 9 | log = Monitor.getLogger('Probe'), 10 | stat = Monitor.getStatLogger('Probe'), 11 | Cron = Monitor.Cron, _ = Monitor._, Backbone = Monitor.Backbone; 12 | 13 | /** 14 | * A software device used to expose real time data to monitors 15 | * 16 | * This is the base class from which all probe implementations extend. 17 | * 18 | * In order to send probe data to monitors, probe implementations simply set 19 | * their model data using ```set()```. Those changes are detected and propagated 20 | * to all monitors of this probe, firing their change events. 21 | * 22 | * In order to allow remote probe control, probes need only provide a method 23 | * called ```{name}_control()```. See the ```ping_control()``` method as an example, 24 | * and the ```Probe.onControl()``` method for more information. 25 | * 26 | * @class Probe 27 | * @extends Backbone.Model 28 | * @constructor 29 | * @param model - Initial data model. Can be a JS object or another Model. 30 | * @param model.id {String} The probe id. 31 | * Assigned by the Router on probe instantiation. 32 | */ 33 | var Probe = Monitor.Probe = Backbone.Model.extend({ 34 | 35 | defaults: { 36 | id: null 37 | }, 38 | 39 | /** 40 | * Initialize the probe 41 | * 42 | * This is called on the probe during construction. It contains 43 | * the probe initialization attributes and an option to make probe 44 | * construction asynchronous. 45 | * 46 | * Probe implementations can defer the initial response to the monitor until 47 | * the initial state is loaded. This allows the callback on 48 | * ```Monitor.connect()``` 49 | * to have the complete initial state of the probe when called. 50 | * 51 | * If the initial probe state cannot be determined in ```initialize```, it should 52 | * set the ```options.asyncInit``` option to ```true```, and call the 53 | * ```options.callback(error)``` once the initial state is determined. 54 | * 55 | * // Asynchronous initialization 56 | * options.asyncInit = true; 57 | * var callback = options.callback 58 | * 59 | * If ```asyncInit``` is set to true, the ```callback``` must be called once 60 | * the initial state of the probe is known (or in an error condition). 61 | * 62 | * // Set the initial state, and call the callback 63 | * this.set(...); 64 | * callback(null); 65 | * 66 | * See the ```initialize``` 67 | * method of the FileProbe probe for an example. It defers 68 | * returning the probe to the monitor until the initial file contents are loaded. 69 | * 70 | * @method initialize 71 | * @param attributes {Object} Initial probe attributes sent in from the Monitor 72 | * @param options {Object} Initialization options 73 | * @param options.asyncInit {boolean} Set this to TRUE if the initial probe 74 | * state can't be known immediately. 75 | * @param options.callback {function(error)} The callback to call 76 | * if asyncInit is set to true. If an error is passed, the probe 77 | * will not be used. 78 | */ 79 | initialize: function(attributes, options) { 80 | var t = this; 81 | log.info('init', t.toJSON(), options); 82 | }, 83 | 84 | /** 85 | * Release any resources consumed by this probe. 86 | * 87 | * This can be implemented by derived classes that need to be informed when 88 | * they are to be shut down. 89 | * 90 | * Probes that listen to events should use this method to remove their 91 | * event listeners. 92 | * 93 | * @method release 94 | */ 95 | release: function(){ 96 | var t = this; 97 | log.info('release', t.toJSON()); 98 | }, 99 | 100 | /** 101 | * Dispatch a control message to the appropriate control function. 102 | * 103 | * This is called when the 104 | * ```control()``` 105 | * method of a monitor is called. 106 | * The name determines the method name called on the probe. 107 | * 108 | * The probe must implement a method with the name ```{name}_control()```, 109 | * and that method must accept two parameters - an input params and a callback. 110 | * The callback must be called, passing an optional error and response object. 111 | * 112 | * For example, if the probe supports a control with the name ```go```, then 113 | * all it needs to do is implement the ```go_control()``` method with the 114 | * proper signature. See ```ping_control()``` for an example. 115 | * 116 | * @method onControl 117 | * @param name {String} Name of the control message. 118 | * @param [params] {Any} Input parameters specific to the control message. 119 | * @param [callback] {Function(error, response)} Called to send the message (or error) response. 120 | *
    121 | *
  • error (Any) An object describing an error (null if no errors)
  • 122 | *
  • response (Any) Response parameters specific to the control message. 123 | *
124 | */ 125 | onControl: function(name, params, callback) { 126 | var t = this, 127 | controlFn = t[name + '_control'], 128 | startTime = Date.now(), 129 | errMsg, 130 | logId = 'onControl.' + t.probeClass + '.' + name; 131 | 132 | params = params || {}; 133 | callback = callback || function(){}; 134 | log.info(logId, t.get('id'), params); 135 | 136 | if (!controlFn) { 137 | errMsg = 'No control function: ' + name; 138 | log.error(logId, errMsg); 139 | return callback({msg: errMsg}); 140 | } 141 | 142 | var whenDone = function(error) { 143 | if (error) { 144 | log.error(logId + '.whenDone', error); 145 | return callback(error); 146 | } 147 | var duration = Date.now() - startTime; 148 | log.info(logId, params); 149 | stat.time(t.logId, duration); 150 | callback.apply(null, arguments); 151 | }; 152 | 153 | // Run the control on next tick. This provides a consistent callback 154 | // chain for local and remote probes. 155 | setTimeout(function(){ 156 | try { 157 | controlFn.call(t, params, whenDone); 158 | } catch (e) { 159 | errMsg = 'Error calling control: ' + t.probeClass + ':' + name; 160 | whenDone({msg:errMsg, err: e.toString()}); 161 | } 162 | }, 0); 163 | }, 164 | 165 | /** 166 | * Remotely set a probe attribute. 167 | * 168 | * This allows setting probe attributes that are listed in writableAttributes. 169 | * It can be overwritten in derived Probe classes for greater control. 170 | * 171 | * @method set_control 172 | * @param attrs {Object} Name/Value attributes to set. All must be writable. 173 | * @param callback {Function(error)} Called when the attributes are set or error 174 | */ 175 | set_control: function(attrs, callback) { 176 | var t = this, 177 | writableAttributes = t.get('writableAttributes') || []; 178 | 179 | // Validate the attributes are writable 180 | if (writableAttributes !== '*') { 181 | for (var attrName in attrs) { 182 | if (writableAttributes.indexOf(attrName) < 0) { 183 | return callback({code:'NOT_WRITABLE', msg: 'Attribute not writable: ' + attrName}); 184 | } 185 | } 186 | } 187 | 188 | // Set the data 189 | var error = null; 190 | if (!t.set(attrs)) { 191 | error = {code:'VALIDATION_ERROR', msg:'Data set failed validation'}; 192 | log.warn('set_control', error); 193 | } 194 | return callback(error); 195 | }, 196 | 197 | /** 198 | * Respond to a ping control sent from a monitor 199 | * 200 | * @method ping_control 201 | * @param params {Object} Input parameters (not used) 202 | * @param callback {Function(error, response)} Called to send the message (or error) response. 203 | *
    204 | *
  • error (Any) An object describing an error
  • 205 | *
  • response (String) The string 'pong' is returned as the response
  • 206 | *
207 | */ 208 | ping_control: function(params, callback) { 209 | return callback(null, 'pong'); 210 | } 211 | 212 | }); 213 | 214 | // Register probe classes when loaded 215 | Probe.classes = {}; // key = name, data = class definition 216 | Probe.extend = function(params) { 217 | var t = this, probeClass = Backbone.Model.extend.apply(t, arguments); 218 | if (params.probeClass) {Probe.classes[params.probeClass] = probeClass;} 219 | return probeClass; 220 | }; 221 | 222 | /** 223 | * Constructor for a list of Probe objects 224 | * 225 | * var myList = new Probe.List(initialElements); 226 | * 227 | * @static 228 | * @method List 229 | * @param [items] {Array} Initial list items. These can be raw JS objects or Probe data model objects. 230 | * @return {Backbone.Collection} Collection of Probe data model objects 231 | */ 232 | Probe.List = Backbone.Collection.extend({model: Probe}); 233 | 234 | }(this)); 235 | -------------------------------------------------------------------------------- /lib/Server.js: -------------------------------------------------------------------------------- 1 | // Server.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // Module loading 8 | var Monitor = root.Monitor || require('./Monitor'), 9 | Config = Monitor.Config, _ = Monitor._, Backbone = Monitor.Backbone, 10 | log = Monitor.getLogger('Server'), 11 | stat = Monitor.getStatLogger('Server'), 12 | Connection = Monitor.Connection, 13 | Http = Monitor.commonJS ? require('http') : null, 14 | SocketIO = root.io || require('socket.io'); 15 | 16 | /** 17 | * A server for accepting inbound connections from remote monitors 18 | * 19 | * Servers are created when a process wants to expose probe data to remote 20 | * monitors. Example: 21 | * 22 | * // Accept remote monitors 23 | * var server = new Monitor.Server(); 24 | * server.start(); 25 | * 26 | * An instance of this class represents a listening server accepting inbound 27 | * connections. As inbound connections are detected, a new 28 | * Connection object is created to manage 29 | * traffic on that connection. 30 | * 31 | * Security: Make sure the port range specified in Monitor.Config (starting 32 | * at 42000) is not exposed outside your internal network. If you desire a 33 | * different security model, create your secure server and pass it to the 34 | * constructor. 35 | * 36 | * @class Server 37 | * @extends Backbone.Model 38 | * @constructor 39 | * @param model - Initial data model. Can be a JS object or another Model. 40 | * @param model.gateway {Boolean} - Allow incoming monitors to use me as a gateway (default false) 41 | * @param model.server {HttpServer} - The listening node.js server. Constructed by this class, or specified if a custom server is desired. 42 | * @param model.port {Integer} - The connected port. This is set upon start() if the server isn't specified on construction. 43 | */ 44 | var Server = Monitor.Server = Backbone.Model.extend({ 45 | 46 | initialize: function(params) { 47 | var t = this; 48 | t.isListening = false; 49 | t.connections = new Connection.List(); 50 | }, 51 | 52 | /** 53 | * Start accepting monitor connections 54 | * 55 | * This method starts listening for incoming monitor connections on the 56 | * server. 57 | * 58 | * If the server was specified during object creation, this binds the 59 | * socket.io service to the server. 60 | * 61 | * If the server was not specified during object creation, this will create 62 | * a server on the first available monitor port. 63 | * 64 | * @method start 65 | * @param options {Object} - Start options. OPTIONAL 66 | * @param options.port {Integer} - Port to attempt listening on if server isn't specified. Default: 42000 67 | * @param options.attempt {Integer} - Attempt number for internal recursion detection. Default: 1 68 | * @param callback {Function(error)} - Called when the server is accepting connections. 69 | */ 70 | /** 71 | * The server has started 72 | * 73 | * This event is fired when the server has determined the port to accept 74 | * connections on, and has successfully configured the server to start 75 | * accepting new monitor connections. 76 | * 77 | * @event start 78 | */ 79 | /** 80 | * A client error has been detected 81 | * 82 | * This event is fired if an error has been detected in the underlying 83 | * transport. It may indicate message loss, and may result in a 84 | * subsequent stop event if the connection cannot be restored. 85 | * 86 | * @event error 87 | */ 88 | start: function(options, callback) { 89 | if (typeof options === 'function') { 90 | callback = options; 91 | options = null; 92 | } 93 | options = options || {}; 94 | callback = callback || function(){}; 95 | var t = this, server = t.get('server'), error, 96 | startTime = Date.now(), 97 | port = options.port || Config.Monitor.serviceBasePort, 98 | attempt = options.attempt || 1, 99 | allowExternalConnections = Config.Monitor.allowExternalConnections; 100 | 101 | // Recursion detection. Only scan for so many ports 102 | if (attempt > Config.Monitor.portsToScan) { 103 | error = {err:'connect:failure', msg: 'no ports available'}; 104 | log.error('start', error); 105 | return callback(error); 106 | } 107 | 108 | // Bind to an existing server, or create a new server 109 | if (server) { 110 | t.bindEvents(callback); 111 | } else { 112 | server = Http.createServer(); 113 | 114 | // Try next port if a server is listening on this port 115 | server.on('error', function(err) { 116 | if (err.code === 'EADDRINUSE') { 117 | // Error if the requested port is in use 118 | if (t.get('port')) { 119 | log.error('portInUse',{host:host, port:port}); 120 | return callback({err:'portInUse'}); 121 | } 122 | // Try the next port 123 | log.info('portInUse',{host:host, port:port}); 124 | return t.start({port:port + 1, attempt:attempt + 1}, callback); 125 | } 126 | // Unknown error 127 | callback(err); 128 | }); 129 | 130 | // Allow connections from INADDR_ANY or LOCALHOST only 131 | var host = allowExternalConnections ? '0.0.0.0' : '127.0.0.1'; 132 | 133 | // Start listening, callback on success 134 | server.listen(port, host, function(){ 135 | 136 | // Set a default NODE_APP_INSTANCE based on the available server port 137 | if (!process.env.NODE_APP_INSTANCE) { 138 | process.env.NODE_APP_INSTANCE = '' + (port - Config.Monitor.serviceBasePort + 1); 139 | } 140 | 141 | // Record the server & port, and bind incoming events 142 | t.set({server: server, port: port}); 143 | t.bindEvents(callback); 144 | log.info('listening', { 145 | appName: Config.Monitor.appName, 146 | NODE_APP_INSTANCE: process.env.NODE_APP_INSTANCE, 147 | listeningOn: host, 148 | port: port 149 | }); 150 | }); 151 | } 152 | }, 153 | 154 | /** 155 | * Bind incoming socket events to the server 156 | * 157 | * This method binds to the socket events and attaches the socket.io 158 | * server. It is called when the connection starts listening. 159 | * 160 | * @protected 161 | * @method bindEvents 162 | * @param callback {Function(error)} - Called when all events are bound 163 | */ 164 | bindEvents: function(callback) { 165 | 166 | // Detect server errors 167 | var t = this, server = t.get('server'); 168 | server.on('clientError', function(err){ 169 | log.error('bindEvents', 'clientError detected on server', err); 170 | t.trigger('error', err); 171 | }); 172 | server.on('close', function(err){ 173 | server.hasEmittedClose = true; 174 | log.info('bindEvents.serverClose', 'Server has closed', err); 175 | t.stop(); 176 | }); 177 | 178 | // Start up the socket.io server. 179 | var socketIoParams = { 180 | log: false 181 | }; 182 | t.socketServer = SocketIO.listen(server, socketIoParams); 183 | t.socketServer.sockets.on('connection', function (socket) { 184 | var connection = Monitor.getRouter().addConnection({ 185 | socket: socket, gateway: t.get('gateway') 186 | }); 187 | t.connections.add(connection); 188 | var onDisconnect = function(reason) { 189 | t.connections.remove(connection); 190 | Monitor.getRouter().removeConnection(connection); 191 | connection.off('disconnect', onDisconnect); 192 | log.info('client.disconnect', 'Disconnected client socket'); 193 | }; 194 | connection.on('disconnect', onDisconnect); 195 | log.info('client.connect', 'Connected client socket'); 196 | }); 197 | 198 | // Notify that we've started 199 | t.isListening = true; 200 | if (callback) {callback(null);} 201 | t.trigger('start'); 202 | }, 203 | 204 | /** 205 | * Stop processing inbound monitor traffic 206 | * 207 | * This method stops accepting new inbound monitor connections, and closes 208 | * all existing monitor connections associated with the server. 209 | * 210 | * @method stop 211 | * @param callback {Function(error)} - Called when the server has stopped 212 | */ 213 | /** 214 | * The server has stopped 215 | * 216 | * This event is fired after the server has stopped accepting inbound 217 | * connections, and has closed all existing connections and released 218 | * associated resources. 219 | * 220 | * @event stop 221 | */ 222 | stop: function(callback) { 223 | var t = this, server = t.get('server'), router = Monitor.getRouter(); 224 | callback = callback || function(){}; 225 | 226 | // Call the callback, but don't stop more than once. 227 | if (!t.isListening) { 228 | return callback(); 229 | } 230 | 231 | // Release resources 232 | t.connections.each(router.removeConnection, router); 233 | t.connections.reset(); 234 | 235 | // Shut down the server 236 | t.isListening = false; 237 | server.close(); 238 | 239 | // Send notices 240 | t.trigger('stop'); 241 | return callback(); 242 | } 243 | }); 244 | 245 | /** 246 | * Constructor for a list of Server objects 247 | * 248 | * var myList = new Server.List(initialElements); 249 | * 250 | * @static 251 | * @method List 252 | * @param [items] {Array} Initial list items. These can be raw JS objects or Server data model objects. 253 | * @return {Backbone.Collection} Collection of Server data model objects 254 | */ 255 | Server.List = Backbone.Collection.extend({model: Server}); 256 | 257 | }(this)); 258 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // index.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | /* 8 | * Entry point for commonJS style loading 9 | * 10 | * This file coordinates the loading of modules in a consistent order 11 | * in a commonJS environment. 12 | */ 13 | 14 | var commonJS = (typeof exports !== 'undefined'); 15 | if (commonJS) { 16 | 17 | // Only load once 18 | if (global.Monitor) { 19 | module.exports = global.Monitor; 20 | } 21 | else { 22 | 23 | // Export the Monitor class to module and global scope to assure 24 | // a single load, and to match the browser-side global Monitor. 25 | var Monitor = global.Monitor = module.exports = require('./Monitor'); 26 | 27 | // Attach backbone callbacks 28 | require('backbone-callbacks').attach(Monitor.Backbone); 29 | 30 | // Grunt.js contains the module definition files 31 | var MODULE_DEF = require('../grunt.js').MODULE_DEF; 32 | 33 | // Load local library files, then server-only probes 34 | var allFiles = MODULE_DEF.lib.concat(MODULE_DEF.probes); 35 | allFiles.forEach(function(file) {require('../' + file);}); 36 | } 37 | } 38 | 39 | }(this)); 40 | -------------------------------------------------------------------------------- /lib/probes/DataModelProbe.js: -------------------------------------------------------------------------------- 1 | // DataModelProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | 6 | (function(root){ 7 | 8 | // Module loading - this runs server-side only 9 | var Monitor = root.Monitor || require('../Monitor'); 10 | 11 | /** 12 | * Probe representation of a simple data model 13 | * 14 | * This probe allows remote creation, manipulation, and change moitoring for 15 | * arbitrary data. It is useful for monitor applications needing to maintain 16 | * a small amount of state on the system being monitored. 17 | * 18 | * @class DataModelProbe 19 | * @extends Probe 20 | * @constructor 21 | * @param [initParams] - Initialization parameters. An object containing the 22 | * initial state of the data model. All properties become data model 23 | * elements, readable and writable by all monitors connected to the probe. 24 | */ 25 | var DataModelProbe = Monitor.DataModelProbe = Monitor.Probe.extend({ 26 | 27 | // These are required for Probes 28 | probeClass: 'DataModel', 29 | writableAttributes: '*' 30 | 31 | }); 32 | 33 | }(this)); 34 | -------------------------------------------------------------------------------- /lib/probes/FileSyncProbe.js: -------------------------------------------------------------------------------- 1 | // FileSyncProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // Module loading - this runs server-side only 8 | var Monitor = root.Monitor || require('../Monitor'), 9 | logger = Monitor.getLogger('FileSyncProbe'), 10 | Probe = Monitor.Probe, 11 | _ = Monitor._, 12 | SyncProbe = Monitor.SyncProbe, 13 | FS = require('fs'), 14 | Path = require('path'), 15 | FileProbe = Monitor.FileProbe; 16 | 17 | // This must be set using setRootPath() before the probe will operate 18 | var ROOT_PATH = null; 19 | 20 | /** 21 | * Probe for synchronizing a Backbone data model with a file on the O/S 22 | * 23 | * Probe parameters are listed under SyncProbe. 24 | * 25 | * @class FileSyncProbe 26 | * @extends Probe 27 | * @constructor 28 | */ 29 | var FileSyncProbe = SyncProbe.FileSyncProbe = Probe.extend({ 30 | 31 | probeClass: 'FileSyncProbe', 32 | 33 | initialize: function(attributes, options){ 34 | var t = this; 35 | Probe.prototype.initialize.apply(t, arguments); 36 | 37 | // Disable the probe if the root path hasn't been set 38 | if (!ROOT_PATH) { 39 | throw new Error('FileSync has not been enabled on this server.'); 40 | } 41 | 42 | // Class name must exist 43 | if (!t.has('className')) { 44 | throw new Error('FileSync - Class name not specified'); 45 | } 46 | 47 | // Don't allow a path above the root path 48 | t.dirPath = Path.join(ROOT_PATH, t.get('className')); 49 | if (t.dirPath.indexOf(ROOT_PATH) !== 0) { 50 | throw new Error('Invalid file path'); 51 | } 52 | 53 | // We're done if this isn't a liveSync probe 54 | if (!t.has('modelId')) { 55 | return; 56 | } 57 | 58 | // Assume callback responsibility 59 | options.asyncInit = true; 60 | var callback = options.callback; 61 | 62 | // Get the full path to the file 63 | t.getFullPath(t.get('modelId'), function(error, response){ 64 | if (error) { 65 | return callback({msg: 'Failed to get the path', err:error}); 66 | } 67 | 68 | // Get the file and stats 69 | var fullPath = response.path; 70 | var stats = response.stats; 71 | 72 | // Build the function to watch the file 73 | var onFileWatch = function(error, content) { 74 | 75 | var isInitializing = (callback !== null), 76 | initCallback = callback; 77 | callback = null; 78 | 79 | if (error && error.code === 'ENOENT') { 80 | // File doesn't exist. Set the model to null. 81 | t.set({model: {}}, {silent: isInitializing}); 82 | // Convert the code from the sync probe spec 83 | error.code = 'NOTFOUND'; 84 | } 85 | if (error) { 86 | if (isInitializing) { 87 | t.release(); 88 | var err = {code: error.code, msg: 'LiveSync requires the file to exist and be readable'}; 89 | initCallback(err); 90 | } 91 | return; 92 | } 93 | 94 | // Parse the JSON content into a JS object. 95 | try { 96 | content = JSON.parse(content); 97 | logger.info('fileParse', {id: t.get('modelId'), content: content}); 98 | } catch (e) { 99 | 100 | // Fail the probe on first load error 101 | if (isInitializing) { 102 | t.release(); 103 | initCallback({code: 'BAD_FORMAT', msg: 'Non-JSON formatted file'}); 104 | } 105 | 106 | // Nothing productive to do if the file can't be parsed. Just log it. 107 | logger.error('fileParse', {error: e, id: t.get('modelId'), content: content}); 108 | return; 109 | } 110 | 111 | // Set the content into the model if it's different 112 | // Have to compare raw objects because toJSON returns deep references to models 113 | var priorModel = t.get('model'); 114 | if (!priorModel || !_.isEqual(content, JSON.parse(JSON.stringify(priorModel)))) { 115 | t.set({model: content}, {silent: isInitializing}); 116 | } 117 | 118 | // Call the initialization callback on first load 119 | if (isInitializing) { 120 | initCallback(); 121 | } 122 | }; 123 | 124 | // Load and watch the file 125 | var watcherOpts = { 126 | preload: true, 127 | persistent: true 128 | }; 129 | t.fileWatcher = FileProbe.watchLoad(fullPath, watcherOpts, onFileWatch); 130 | 131 | }); 132 | }, 133 | 134 | // Documentation for these methods in SyncProbe 135 | create_control: function(args, callback) { 136 | // Make sure the ID exists 137 | var t = this, model = args.model; 138 | if (!model || !model.id) { 139 | return callback({msg:'SyncProbe create - Data model with ID not present'}); 140 | } 141 | 142 | // Make sure the file doesn't already exist 143 | t.getFullPath(model.id, function(error, response) { 144 | if (error) { 145 | return callback(error); 146 | } 147 | 148 | if (response.stats) { 149 | return callback({msg:'Document with this ID already exists'}); 150 | } 151 | 152 | // Forward to the update control 153 | t.update_control(args, callback); 154 | }); 155 | }, 156 | 157 | read_control: function(args, callback) { 158 | // Make sure the ID exists 159 | var t = this, id = args.id; 160 | if (!id) { 161 | return callback({msg:'SyncProbe read - ID not present'}); 162 | } 163 | 164 | // Read the file 165 | t.getFullPath(id, function(error, response){ 166 | if (error) { 167 | return callback(error); 168 | } 169 | if (!response.stats) { 170 | return callback({code: 'NOTFOUND', msg:'Document with this ID not found'}); 171 | } 172 | 173 | var fullPath = response.path; 174 | FS.readFile(fullPath, 'utf8', function(error, data) { 175 | if (error) { 176 | return callback({code: 'UNKNOWN', msg:'Error reading file', error: error.code}); 177 | } 178 | 179 | // Parse the file 180 | var model; 181 | try { 182 | model = JSON.parse(data); 183 | } catch (e) { 184 | return callback({code: 'PARSE', msg: 'Error parsing file'}); 185 | } 186 | callback(null, model); 187 | }); 188 | }); 189 | }, 190 | 191 | update_control: function(args, callback) { 192 | 193 | // Make sure the ID exists 194 | var t = this, model = args.model; 195 | if (!model || !model.id) { 196 | return callback({msg:'SyncProbe create - Data model with ID not present'}); 197 | } 198 | 199 | // Make sure the directory exists 200 | t.getFullPath(model.id, function(error, response) { 201 | if (error) { 202 | return callback(error); 203 | } 204 | 205 | var fullPath = response.path, 206 | parentDir = Path.dirname(fullPath); 207 | FileProbe.mkdir_r(parentDir, function(error) { 208 | if (error) { 209 | return callback(error); 210 | } 211 | 212 | // Set the contents of the model for liveSync 213 | if (t.has('modelId')) { 214 | t.set('model', model); 215 | } 216 | 217 | // Write the file 218 | FS.writeFile(fullPath, JSON.stringify(model, null, 2), 'utf8', function(error){ 219 | callback(error, {}); 220 | }); 221 | }); 222 | }); 223 | 224 | }, 225 | 226 | delete_control: function(args, callback) { 227 | // Make sure the ID exists 228 | var t = this, id = args.id; 229 | if (!id) { 230 | return callback({msg:'SyncProbe delete - ID not present'}); 231 | } 232 | 233 | // Set the contents of the model for liveSync 234 | t.getFullPath(id, function(error, response) { 235 | if (error) { 236 | return callback({msg:'Error removing file', err:error}); 237 | } 238 | var fullPath = response.path; 239 | if (t.has('modelId')) { 240 | t.set('model', null); 241 | } 242 | 243 | // Remove the file 244 | FS.unlink(fullPath, function(error, data) { 245 | if (error) { 246 | return callback({msg:'Error removing file'}); 247 | } 248 | return callback(null, {}); 249 | }); 250 | }); 251 | }, 252 | 253 | release: function() { 254 | var t = this; 255 | if (t.fileWatcher) { 256 | t.fileWatcher.close(); 257 | t.fileWatcher = null; 258 | } 259 | }, 260 | 261 | /** 262 | * Get the full path to the file 263 | * 264 | * This builds the full pathname to the file, and performs an fs.sync() 265 | * on that pathname, providing the pathname and sync object in the callback. 266 | * 267 | * @method getFullPath 268 | * @param modelId {String} ID of the data model to sync 269 | * @param callback {Function(error, return)} 270 | * @param callback.error {Object} Error object (null if no error) 271 | * @param callback.return {Object} return object 272 | * @param callback.return.path {String} Full pathname to the file 273 | * @param callback.return.stat {fs.stats} Stats object (null if the file doesn't esixt) 274 | */ 275 | getFullPath: function(modelId, callback) { 276 | var t = this, 277 | dirPath = t.dirPath; 278 | 279 | // Don't allow relative paths 280 | var fullPath = Path.join(t.dirPath, modelId); 281 | if (fullPath.indexOf(dirPath) !== 0) { 282 | return callback({msg: 'Model ID ' + modelId + ' cannot represent a relative path'}); 283 | } 284 | 285 | // See if the path represents a directory 286 | FS.stat(fullPath, function(error, stats){ 287 | 288 | // If this is an existing directory, return a path to dir/index.json 289 | if (!error && stats.isDirectory()) { 290 | return t.getFullPath(modelId + '/index', callback); 291 | } 292 | 293 | // Normal case - return the path & stat to the json file 294 | fullPath += '.json'; 295 | FS.stat(fullPath, function(error, stats){ 296 | 297 | // Not an error if error == ENOENT 298 | if (error && error.code === 'ENOENT') { 299 | error = null; stats = null; 300 | } 301 | 302 | // Process other FS errors 303 | if (error) { 304 | return callback({err: error, msg: "Error while observing file: " + fullPath}); 305 | } 306 | 307 | // Forward the callback 308 | return callback(null, {path: fullPath, stats: stats}); 309 | }); 310 | }); 311 | } 312 | 313 | }); 314 | 315 | /** 316 | * Set the server root path for objects stored with this probe 317 | * 318 | * For security purposes, this must be set before the SyncFileProbe 319 | * will operate. It will not accept any changes once set. 320 | * 321 | * @static 322 | * @method setRootPath 323 | * @param rootPath {String} A path to the root directory for model object storage 324 | */ 325 | FileSyncProbe.setRootPath = function(rootPath) { 326 | var normalized = Path.normalize(rootPath); 327 | if (ROOT_PATH && ROOT_PATH !== normalized) { 328 | throw new Error('Cannot change the File probe root path once set.'); 329 | } 330 | ROOT_PATH = normalized; 331 | }; 332 | 333 | /** 334 | * Get the current root path. 335 | * 336 | * As a static method, this is only available on the server running the probe. 337 | * For security purposes, this is not exposed in the FileSyncProbe data model. 338 | * 339 | * @static 340 | * @method getRootPath 341 | * @return {String} The path to the root directory for the FilePath probe 342 | */ 343 | FileSyncProbe.getRootPath = function() { 344 | return ROOT_PATH; 345 | }; 346 | 347 | }(this)); 348 | -------------------------------------------------------------------------------- /lib/probes/InspectProbe.js: -------------------------------------------------------------------------------- 1 | // InspectProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | 6 | /* This class is evil. You probably shouldn't use it. Or drink. Or drink while using it. */ 7 | /*jslint evil: true */ 8 | 9 | (function(root){ 10 | 11 | // Module loading - this runs server-side only 12 | var Monitor = root.Monitor || require('../Monitor'), 13 | _ = Monitor._, 14 | logger = Monitor.getLogger('InspectProbe'), 15 | Backbone = Monitor.Backbone, 16 | PollingProbe = Monitor.PollingProbe; 17 | 18 | // Constants 19 | var DEFAULT_DEPTH = 2; 20 | 21 | /** 22 | * Inspect and manipulate variables on the monitored server. 23 | * 24 | * This class monitors the variable specified by the key. 25 | * 26 | * The key is evaluated to determine the variable to monitor, so it may 27 | * be a complex key starting at global scope. If the key isn't 28 | * specified, it monitors all variables in the global scope. 29 | * 30 | * If the key points to an object of type Backbone.Model, this probe 31 | * will update the value in real time, triggered on the *change* event. 32 | * Otherwise it will update the value as it notices changes, while polling 33 | * on the specified polling interval (default: 1 second). 34 | * 35 | * @class InspectProbe 36 | * @extends PollingProbe 37 | * @constructor 38 | * @param [initParams] - Initialization parameters 39 | * @param [initParams.key=null] {String} A global variable name or expression 40 | * @param [initParams.depth=2] {Integer} If the key points to an object, this 41 | * is the depth to traverse the object for changes. Default=2, or 1 if 42 | * key='window'. 43 | * @param [initParams.pollInterval] {Integer} (from PollingProbe) Polling interval in milliseconds. Default: null 44 | * @param [initParams.cronPattern] {String} (from PollingProbe) Crontab syle polling pattern. Default once per second: "* * * * * *" 45 | * @param model - Monitor data model elements 46 | * @param model.value - The value of the element being inspected 47 | * @param model.isModel - Is the value a Backbone.Model? 48 | */ 49 | var InspectProbe = Monitor.InspectProbe = PollingProbe.extend({ 50 | 51 | // These are required for Probes 52 | probeClass: 'Inspect', 53 | writableAttributes: ['value'], 54 | 55 | initialize: function(initParams){ 56 | var t = this; 57 | 58 | // Get the global object if the key isn't specified 59 | t.key = initParams.key; 60 | if (typeof initParams.key === 'undefined') { 61 | t.key = typeof window === 'undefined' ? 'global' : 'window'; 62 | } 63 | 64 | // Get a good depth default. Default unless key = window. 65 | if (typeof initParams.depth === 'undefined') { 66 | if (!initParams.key && t.key === 'window') { 67 | t.depth = 1; 68 | } else { 69 | t.depth = DEFAULT_DEPTH; 70 | } 71 | } else { 72 | t.depth = initParams.depth; 73 | } 74 | 75 | // Evaluate the expression to see if it's a Backbone.Model 76 | // This will throw an exception if the key is a bad expression 77 | t.value = t._evaluate(t.key); 78 | t.isModel = t.value instanceof Backbone.Model; 79 | 80 | // Set the initial values 81 | t.set({ 82 | value: Monitor.deepCopy(t.value, t.depth), 83 | isModel: t.isModel 84 | }); 85 | 86 | // Watch for backbone model changes, or initialize the polling probe 87 | if (t.isModel) { 88 | t.value.on('change', t.poll, t); 89 | } else { 90 | PollingProbe.prototype.initialize.apply(t, arguments); 91 | } 92 | }, 93 | 94 | /** 95 | * Remotely set the inspected variable's value 96 | * 97 | * @method set_control 98 | * @param attrs {Object} Name/Value attributes to set. All must be writable. 99 | * @param callback {Function(error)} Called when the attributes are set or error 100 | */ 101 | set_control: function(attrs, callback) { 102 | var t = this; 103 | 104 | // Value is the only thing to set 105 | if (typeof attrs.value === 'undefined') { 106 | return callback({code:'NO_VALUE'}); 107 | } 108 | 109 | // Set the model elements. These cause change events to fire 110 | if (t.isModel) { 111 | t.value.set(attrs.value); 112 | } 113 | else { 114 | // Set the variable directly 115 | var jsonValue = JSON.stringify(attrs.value); 116 | t._evaluate(t.key + ' = ' + jsonValue); 117 | t.set('value', attrs.value); 118 | } 119 | return callback(); 120 | }, 121 | 122 | // Stop watching for change events or polling 123 | release: function() { 124 | var t = this; 125 | if (t.isModel) { 126 | t.value.off('change', t.poll, t); 127 | } else { 128 | PollingProbe.prototype.release.apply(t, arguments); 129 | } 130 | }, 131 | 132 | /** 133 | * Evaluate an expression, returning the depth-limited results 134 | * 135 | * @method eval_control 136 | * @param expression {String} Expression to evaluate 137 | * @param [depth=2] {Integer} Depth of the object to return 138 | * @return value {Mixed} Returns the depth-limited value 139 | */ 140 | eval_control: function(expression, depth){ 141 | var t = this; 142 | 143 | // Determine a default depth 144 | depth = typeof depth === 'undefined' ? DEFAULT_DEPTH : depth; 145 | 146 | // Get the raw value 147 | var value = t._evaluate(expression); 148 | 149 | // Return the depth limited results 150 | return Monitor.deepCopy(value, depth); 151 | }, 152 | 153 | /** 154 | * Evaluate an expression, returning the raw results 155 | * 156 | * @protected 157 | * @method _evaluate 158 | * @param expression {String} Expression to evaluate 159 | * @return value {Mixed} Returns the expression value 160 | */ 161 | _evaluate: function(expression){ 162 | var t = this, 163 | value = null; 164 | 165 | // Evaluate the expression 166 | try { 167 | value = eval(expression); 168 | } catch (e) { 169 | var err = 'Unable to evaluate expression: "' + expression + '"'; 170 | logger.error('evaluate', err); 171 | throw new Error(err); 172 | } 173 | 174 | // Return the value 175 | return value; 176 | }, 177 | 178 | /** 179 | * Poll for changes in the evaluation 180 | * 181 | * @method poll 182 | */ 183 | poll: function() { 184 | var t = this, 185 | newValue = t.eval_control(t.key, t.depth); 186 | 187 | // Set the new value if it has changed from the current value 188 | if (!_.isEqual(newValue, t.get('value'))) { 189 | t.set({value: newValue}); 190 | } 191 | } 192 | }); 193 | 194 | }(this)); 195 | -------------------------------------------------------------------------------- /lib/probes/LogProbe.js: -------------------------------------------------------------------------------- 1 | // LogProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root) { 6 | 7 | // Module loading - this runs server-side only 8 | var Monitor = root.Monitor || require('../Monitor'), 9 | _ = Monitor._, 10 | StreamProbe = Monitor.StreamProbe, 11 | Log = Monitor.Log; 12 | 13 | // Constants 14 | var DEFAULT_PATTERN = '*'; 15 | 16 | /** 17 | * Remote application log monitoring 18 | * 19 | * This probe forwards application logs to the monitor. 20 | * 21 | * @class LogProbe 22 | * @extends StreamProbe 23 | * @constructor 24 | * @param [initParams] {Object} Probe initialization parameters 25 | * @param [initParams.pattern=*] {String} Log name pattern to monitor (see Log) 26 | * @param [initParams.interval=1000] {Numeric} Queue interval (see StreamProbe) 27 | * @param model {Object} Monitor data model elements 28 | * @param model.bundle {Log array} Array of Log elements. 29 | * @param model.bundle.timestamp {String} Timestamp of the log statement 30 | * @param model.bundle.logType {String} Log type (error, info, etc) 31 | * @param model.bundle.module {String} Module that emitted the log 32 | * @param model.bundle.name {String} Log entry name 33 | * @param model.bundle.args {any[]} Arguments to the log statement 34 | * @param model.sequence {Integer} A numeric incrementer causing a change event 35 | */ 36 | var LogProbe = Monitor.LogProbe = StreamProbe.extend({ 37 | 38 | probeClass: 'Log', 39 | 40 | defaults: _.extend({}, StreamProbe.prototype.defaults, { 41 | pattern: DEFAULT_PATTERN 42 | }), 43 | 44 | initialize: function(){ 45 | var t = this; 46 | 47 | // Call parent constructor 48 | StreamProbe.prototype.initialize.apply(t, arguments); 49 | 50 | // The watcher just forwards all args to queueItem as an array 51 | t.watcher = function() { 52 | // Add timestamp as the first element 53 | var logElems = _.toArray(arguments); 54 | logElems.splice(0,0,JSON.stringify(new Date()).substr(1,24)); 55 | t.queueItem.call(t, logElems); 56 | }; 57 | Log.on(t.get('pattern'), t.watcher); 58 | }, 59 | 60 | release: function() { 61 | var t = this; 62 | Log.off(t.get('pattern'), t.watcher); 63 | } 64 | 65 | }); 66 | 67 | }(this)); 68 | -------------------------------------------------------------------------------- /lib/probes/PollingProbe.js: -------------------------------------------------------------------------------- 1 | // PollingProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // Module loading 8 | var Monitor = root.Monitor || require('../Monitor'), 9 | _ = Monitor._, 10 | Cron = Monitor.Cron, 11 | Probe = Monitor.Probe, 12 | Backbone = Monitor.Backbone; 13 | 14 | // Constants 15 | var DEFAULT_POLL_INTERVAL = 1000; 16 | var DEFAULT_CRON_PATTERN = "* * * * * *"; 17 | 18 | /** 19 | * ## Base class for probes that require polling to detect and set model changes. 20 | * 21 | * The probe wakes up every polling interval and executes the poll() method 22 | * in the derived class. 23 | * 24 | * PollingProbes are instantiated with either a polling interval (in milliseconds) 25 | * or a cron pattern. If the polling interval is set, that's what will be used. 26 | * 27 | * The cronPattern isn't available in browser-side probes. 28 | * 29 | * To disable polling, set the pollInterval to 0. 30 | * 31 | * More about cron formats, with examples 32 | * 37 | * 38 | * @class PollingProbe 39 | * @extends Probe 40 | * @constructor 41 | * @param [initParams] {Object} Probe initialization parameters 42 | * @param [initParams.pollInterval] {Integer} Polling interval in milliseconds. Default: null 43 | * @param [initParams.cronPattern] {String} Crontab syle polling pattern. Default once per second: "* * * * * *" 44 | * 45 | * The format is: [second] [minute] [hour] [day of month] [month] [day of week].
46 | */ 47 | var PollingProbe = Monitor.PollingProbe = Probe.extend({ 48 | defaults: _.extend({}, Probe.prototype.defaults, { 49 | pollInterval: null, 50 | cronPattern: DEFAULT_CRON_PATTERN 51 | }), 52 | initialize: function(){ 53 | var t = this, 54 | pollInterval = t.get('pollInterval'), 55 | cronPattern = t.get('cronPattern'), 56 | poll = function(){t.poll();}; 57 | Probe.prototype.initialize.apply(t, arguments); 58 | 59 | // Override cron for the default 1-second interval 60 | // (this allows the default to work when Cron isn't available) 61 | if (pollInterval == null && cronPattern === DEFAULT_CRON_PATTERN) { 62 | pollInterval = DEFAULT_POLL_INTERVAL; 63 | } 64 | 65 | // Poll once, then set up the interval 66 | t.poll(); 67 | if (pollInterval !== 0) { 68 | if (pollInterval) { 69 | t.timer = setInterval(poll, pollInterval); 70 | } else { 71 | if (!Cron) { 72 | throw new Error("Cron is not available in this client"); 73 | } 74 | t.cronJob = new Cron.CronJob(cronPattern, poll); 75 | } 76 | } 77 | }, 78 | release: function(){ 79 | var t = this, timer = (t.cronJob ? t.cronJob.timer : t.timer); 80 | if (t.cronJob && !t.cronJob.initiated) { 81 | // If cron isn't initiated we've been asked to shut down within the 82 | // first second, and the timer hasn't been set (but will be soon). 83 | setTimeout(function(){clearInterval(t.cronJob.timer);}, 1000); 84 | } else if (t.timer) { 85 | clearInterval(timer); 86 | } 87 | t.timer = t.cron = null; 88 | Probe.prototype.release.apply(t, arguments); 89 | } 90 | 91 | }); 92 | 93 | }(this)); 94 | -------------------------------------------------------------------------------- /lib/probes/ProcessProbe.js: -------------------------------------------------------------------------------- 1 | // ProcessProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // Module loading - this runs server-side only 8 | var util = require('util'), OS = require('os'), 9 | Monitor = root.Monitor || require('../Monitor'), _ = Monitor._, 10 | logger = Monitor.getLogger('ProcessProbe'), 11 | PollingProbe = Monitor.PollingProbe; 12 | 13 | /** 14 | * Probe for attaining process and O/S information 15 | * 16 | * @class ProcessProbe 17 | * @extends PollingProbe 18 | * @constructor 19 | * @param [initParams] {Object} Probe initialization parameters (from PollingProbe) 20 | * @param [initParams.pollInterval] {Integer} Polling interval in milliseconds. Default: null 21 | * @param [initParams.cronPattern] {String} Crontab syle polling pattern. Default once per second: "* * * * * *" 22 | * @param model {Object} Monitor data model elements 23 | * @param model.platform {String} O/S Platform 24 | * @param model.version {String} Node.js compiled-in version 25 | * @param model.installPrefix {String} Node.js installation directory 26 | * @param model.title {String} The current process title (as reported in ps) 27 | * @param model.execPath {String} The path to the current node.js executable 28 | * @param model.argv {Array(String)} Arguments passed on the command line to this process 29 | * @param model.env {Object} Current environment (inherited) 30 | * @param model.cwd {String} Current working directory 31 | * @param model.uptime {Integer} Number of seconds the process has been up (if available) 32 | * @param model.versions {String} Versions of V8 and dependent libraries (if available) 33 | * @param model.arch {String} Processor architecture (if available) 34 | * @param model.gid {Integer} Process group ID 35 | * @param model.uid {Integer} Process user ID 36 | * @param model.pid {Integer} Unique process ID 37 | * @param model.umask {Integer} The process file mode creation mask 38 | * @param model.memoryUsage {Object} An object describing memory usage of the node.js process 39 | * @param model.memoryUsage.rss {Integer} As defined by process.memoryUsage 40 | * @param model.memoryUsage.vsize {Integer} As defined by process.memoryUsage 41 | * @param model.memoryUsage.heapTotal {Integer} As defined by process.memoryUsage 42 | * @param model.memoryUsage.heapUsed {Integer} As defined by process.memoryUsage 43 | * @param model.os {Object} An object containing O/S information 44 | * @param model.os.hostname {String} Name of the host operating system 45 | * @param model.os.type {String} Operating system type 46 | * @param model.os.release {String} O/S Release version 47 | * @param model.os.uptime {String} O/S Uptime in seconds 48 | * @param model.os.loadavg {Array(Number)} An array containing the 1, 5, and 15 minute load averages 49 | * @param model.os.freemem {Integer} Free O/S memory (in bytes) 50 | * @param model.os.totalmem {Integer} Total O/S memory capacity (in bytes) 51 | * @param model.os.cpus {Array(Object)} An array of objects containing information about each CPU/core installed 52 | */ 53 | var ProcessProbe = Monitor.ProcessProbe = PollingProbe.extend({ 54 | 55 | // These are required for Probes 56 | probeClass: 'Process', 57 | 58 | /* not required 59 | initialize: function(){ 60 | var t = this; 61 | PollingProbe.prototype.initialize.apply(t, arguments); 62 | ... 63 | }, 64 | release: function() { 65 | var t = this; 66 | PollingProbe.prototype.release.apply(t, arguments); 67 | ... // release any resources held 68 | }) 69 | */ 70 | 71 | /** 72 | * Poll the probe for changes 73 | * 74 | * This method is called by the parent PollingProbe on the interval specified by the client Monitor. 75 | * 76 | * It polls for process information, and updates the data model with any changes. 77 | * 78 | * @method poll 79 | */ 80 | poll: function() { 81 | var t = this, 82 | attrs = _.extend({ 83 | platform: process.platform, 84 | version: process.version, 85 | installPrefix: process.installPrefix, 86 | title: process.title, 87 | execPath: process.execPath, 88 | argv: process.argv, 89 | env: process.env, 90 | cwd: process.cwd(), 91 | gid: process.getgid ? process.getgid() : 0, 92 | uid: process.getuid ? process.getuid() : 0, 93 | pid: process.pid, 94 | umask: process.umask(), 95 | hostname: OS.hostname(), 96 | type: OS.type(), 97 | release: OS.release(), 98 | osUptime: OS.uptime(), 99 | loadavg: OS.loadavg(), 100 | freemem: OS.freemem(), 101 | totalmem: OS.totalmem(), 102 | cpus: OS.cpus() 103 | }, process.memoryUsage()); 104 | if (process.uptime) {attrs.uptime = process.uptime();} 105 | if (process.versions) {attrs.versions = process.versions;} 106 | if (process.arch) {attrs.arch = process.arch;} 107 | t.set(attrs); 108 | } 109 | }); 110 | 111 | }(this)); 112 | -------------------------------------------------------------------------------- /lib/probes/RecipeProbe.js: -------------------------------------------------------------------------------- 1 | // RecipeProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | 6 | /* This class is evil. You probably shouldn't use it. Or drink. Or drink while using it. */ 7 | /*jslint evil: true */ 8 | 9 | (function(root){ 10 | 11 | // Module loading - this runs server-side only 12 | var Monitor = root.Monitor || require('../Monitor'), 13 | _ = Monitor._, 14 | Cron = Monitor.Cron, 15 | logger = Monitor.getLogger('RecipeProbe'), 16 | vm = Monitor.commonJS ? require('vm') : null, 17 | Probe = Monitor.Probe; 18 | 19 | /** 20 | * Monitor automation probe 21 | * 22 | * The Recipe probe monitors other probes and runs instructions when the 23 | * probes change, and controls other probes based on these instructions. 24 | * 25 | * It contains a list of monitors to instantiate, and a script to run when the 26 | * monitor ```change``` event is fired. 27 | * 28 | * When the script fires, the monitors are available to the script by name. 29 | * The script can ```get()``` monitor values, ```set()``` writable monitor 30 | * values, and control the monitor using the ```control()`` method. 31 | * 32 | * The ```this``` variable is consistent between script runs, so state can be 33 | * maintained by setting attributes in ```this```. 34 | * 35 | * @class RecipeProbe 36 | * @extends Probe 37 | * @constructor 38 | * @param monitors {Object} - Named list of monitors to instantiate 39 | * Key: monitor variable name, Value: Monitor model parameters 40 | * @param script {String} - JavaScript script to run. 41 | * The script has access to ```console```, ```logger```, and all defined 42 | * monitors by name. 43 | * @param [recipeName] {String} - Recipe name for logging 44 | * @param [autoStart=false] {boolean} - Call the start control on instantiation? 45 | * @param [triggeredBy] {Object} - Trigger the recipe by the items in the object. 46 | * Items can include: 'interval', 'cron', and/or monitorName(s) 47 | * If 'interval' is the key, the value is the interval in milliseconds 48 | * If 'cron' is the key, the value is a string representing the cron pattern 49 | * If any monitor name is the key, the value is the monitor event to trigger on. 50 | * Example: 51 | * triggeredBy: { 52 | * interval: 5000, // This triggers the recipe every 5 seconds 53 | * cron: '* * * * * *', // [second] [minute] [hour] [day of month] [month] [day of week] 54 | * myMonitor: 'change:someAttribute change:someOtherAttribute' 55 | * } 56 | * If triggeredBy isn't specified, any monitor change will trigger the recipe. 57 | * @param [started] {boolean} - Is the recipe started and currently active? 58 | */ 59 | var RecipeProbe = Monitor.RecipeProbe = Probe.extend({ 60 | 61 | probeClass: 'Recipe', 62 | writableAttributes: [], 63 | defaults: { 64 | recipeName: '', 65 | monitors: {}, 66 | script: '', 67 | autoStart: false, 68 | started: false, 69 | triggeredBy: null 70 | }, 71 | 72 | initialize: function(attributes, options){ 73 | var t = this; 74 | 75 | // Periodic triggers 76 | t.interval = null; 77 | t.cronJob = null; 78 | 79 | // Precondition test 80 | if (_.size(t.get('monitors')) === 0) { 81 | logger.error('initialize', 'No monitors defined in the recipe'); 82 | return; 83 | } 84 | 85 | // This is a list of monitors (vs. monitor definitions) 86 | t.monitors = {}; 87 | 88 | // Auto start, calling the callback when started 89 | if (t.get('autoStart')) { 90 | options.asyncInit = true; 91 | t.start_control({}, options.callback); 92 | } 93 | }, 94 | 95 | release: function() { 96 | var t = this, 97 | args = arguments; 98 | t.stop_control({}, function(){ 99 | Probe.prototype.release.apply(t, args); 100 | }); 101 | }, 102 | 103 | /** 104 | * Start the recipe 105 | * 106 | * This connects to each monitor and sets up the recipe triggers 107 | * 108 | * @method start_control 109 | */ 110 | start_control: function(params, callback) { 111 | var t = this, 112 | connectError = false, 113 | monitors = t.get('monitors'); 114 | 115 | if (t.get('started')) { 116 | var err = {code:'RUNNING', msg:'Cannot start - the recipe is already running.'}; 117 | logger.warn(err); 118 | return callback(err); 119 | } 120 | 121 | // Called when a monitor has connected 122 | var onConnect = function(error) { 123 | if (connectError) {return;} 124 | if (error) { 125 | var err = {code:'CONNECT_ERROR', err: error}; 126 | connectError = true; 127 | logger.error('start', err); 128 | return callback(err); 129 | } 130 | for (var name1 in t.monitors) { 131 | if (!t.monitors[name1].isConnected()) { 132 | return; 133 | } 134 | } 135 | t.set({started:true}); 136 | t.connectListeners(true); 137 | callback(); 138 | }; 139 | 140 | // Connect all monitors 141 | for (var name2 in monitors) { 142 | t.monitors[name2] = new Monitor(monitors[name2]); 143 | t.monitors[name2].connect(onConnect); 144 | } 145 | 146 | }, 147 | 148 | /** 149 | * Stop the recipe 150 | * 151 | * This disconnects each monitor 152 | * 153 | * @method stop_control 154 | */ 155 | stop_control: function(params, callback) { 156 | var t = this, 157 | disconnectError = false; 158 | 159 | if (!t.get('started')) { 160 | var err = {code:'NOT_RUNNING', msg:'The recipe is already stopped.'}; 161 | logger.warn('precondition', err); 162 | return callback(err); 163 | } 164 | 165 | // Called when a monitor has disconnected 166 | var onDisconnect = function(error) { 167 | if (disconnectError) {return;} 168 | if (error) { 169 | var err = {code:'DISONNECT_ERROR', err: error}; 170 | disconnectError = true; 171 | logger.error('onDisconnect', err); 172 | return callback(err); 173 | } 174 | for (var name1 in t.monitors) { 175 | if (t.monitors[name1].isConnected()) { 176 | return; 177 | } 178 | } 179 | t.set({started:false}); 180 | t.compiledScript = null; 181 | callback(); 182 | }; 183 | 184 | // Disconnect all monitors 185 | t.connectListeners(false); 186 | t.context = null; 187 | for (var name2 in t.monitors) { 188 | t.monitors[name2].disconnect(onDisconnect); 189 | } 190 | }, 191 | 192 | /** 193 | * Connect the change listeners 194 | * 195 | * @private 196 | * @method connectListeners 197 | */ 198 | connectListeners: function(connect) { 199 | var t = this, 200 | triggeredBy = t.get('triggeredBy'), 201 | onTrigger = t.onTrigger.bind(t); 202 | 203 | // Default to listen on changes to all monitors 204 | if (!triggeredBy) { 205 | for (var monitorName in t.monitors) { 206 | t.monitors[monitorName][connect ? 'on' : 'off']('change', t.onTrigger, t); 207 | } 208 | return; 209 | } 210 | 211 | // Process the elements in triggeredBy 212 | for (var name in triggeredBy) { 213 | var value = triggeredBy[name]; 214 | 215 | // Construct a new cron job 216 | if (name === 'cron') { 217 | if (connect) { 218 | t.cronJob = new Cron.CronJob(value, onTrigger); 219 | } 220 | else { 221 | if (t.cronJob.initiated) { 222 | clearInterval(t.CronJob.timer); 223 | } 224 | else { 225 | setTimeout(function(){clearInterval(t.cronJob.timer);}, 1000); 226 | } 227 | } 228 | } 229 | 230 | // Set a polling interval 231 | else if (name === 'interval') { 232 | if (connect) { 233 | t.interval = setInterval(onTrigger, value); 234 | } 235 | else { 236 | clearInterval(t.interval); 237 | t.interval = null; 238 | } 239 | } 240 | 241 | // Must be a monitor name 242 | else { 243 | t.monitors[name][connect ? 'on' : 'off'](value, onTrigger); 244 | } 245 | } 246 | }, 247 | 248 | /** 249 | * Called when a trigger is fired 250 | * 251 | * @private 252 | * @method onTrigger 253 | */ 254 | onTrigger: function() { 255 | var t = this; 256 | t.run_control({}, function(error){ 257 | if (error) { 258 | logger.error('onTrigger', error); 259 | } 260 | }); 261 | }, 262 | 263 | /** 264 | * Run the recipe script 265 | * 266 | * This manually runs a started recipe. The callback is called immediately 267 | * after executing the script. 268 | * 269 | * @method run_control 270 | */ 271 | run_control: function(params, callback) { 272 | var t = this, 273 | error = null; 274 | if (!t.get('started')) { 275 | error = {code:'NOT_RUNNING', msg:'Cannot run - recipe not started.'}; 276 | logger.warn(error); 277 | return callback(error); 278 | } 279 | 280 | // Name the probe 281 | t.name = t.get('probeName') || t.get('id'); 282 | 283 | // Build a context to pass onto the script. The context contains 284 | // a console, a logger, and each monitor by name. 285 | if (!t.context) { 286 | t.context = vm ? vm.createContext({}) : {}; 287 | t.context.console = console; 288 | t.context.logger = Monitor.getLogger('Recipe.run.' + t.name); 289 | for (var monitorName in t.monitors) { 290 | t.context[monitorName] = t.monitors[monitorName]; 291 | } 292 | } 293 | 294 | // Run the script 295 | try { 296 | t.run(t.context); 297 | } catch(e) { 298 | error = "Error running script: " + e.toString(); 299 | logger.error('run_control', error); 300 | } 301 | callback(error); 302 | }, 303 | 304 | /** 305 | * Execute the recipe. This is a private method that can be overridden 306 | * in derived recipe classes that contain the recipe. 307 | * 308 | * @private 309 | * @method run 310 | */ 311 | run: function(context) { 312 | var t = this, 313 | script = t.get('script'); 314 | 315 | // Run in a VM or exec (if running in a browser) 316 | if (vm) { 317 | // Compile the script on first run. This throws an exception if 318 | // the script has a problem compiling. 319 | if (!t.compiledScript) { 320 | t.compiledScript = vm.createScript(script); 321 | } 322 | 323 | // Execute the compiled script 324 | t.compiledScript.runInContext(context, t.name); 325 | } 326 | else { 327 | // Bring all context variables local, then execute the script 328 | eval(t.bringLocal(context)); 329 | eval(script); 330 | } 331 | }, 332 | 333 | /** 334 | * Generate a script that brings context members into local scope 335 | * 336 | * @private 337 | * @method bringLocal 338 | */ 339 | bringLocal: function(context) { 340 | var varName, 341 | localVars = []; 342 | for (varName in context) { 343 | localVars.push('var ' + varName + ' = context.' + varName + ';'); 344 | } 345 | return localVars.join('\n'); 346 | } 347 | 348 | }); 349 | 350 | 351 | }(this)); 352 | -------------------------------------------------------------------------------- /lib/probes/ReplProbe.js: -------------------------------------------------------------------------------- 1 | // ReplProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // Module loading - this runs server-side only 8 | var Monitor = root.Monitor || require('../Monitor'), 9 | _ = Monitor._, 10 | Probe = Monitor.Probe, 11 | REPL = require('repl'), 12 | Stream = require('stream'), 13 | util = require('util'), 14 | events = require('events'), 15 | ChildProcess = require('child_process'); 16 | 17 | // Statics 18 | var CONSOLE_PROMPT = '> '; 19 | var NEW_REPL = (typeof REPL.disableColors === 'undefined'); 20 | 21 | /** 22 | * A probe based Read-Execute-Print-Loop console for node.js processes 23 | * 24 | * @class ReplProbe 25 | * @extends Probe 26 | * @constructor 27 | * @param initParams {Object} Probe initialization parameters 28 | * @param initParams.uniqueInstance - Usually specified to obtain a unique REPL probe instance 29 | * @param model {Object} Monitor data model elements 30 | * @param model.output {String} Last (current) REPL output line 31 | * @param model.sequence {Integer} Increasing sequence number - to enforce unique line output 32 | */ 33 | var ReplProbe = Monitor.ReplProbe = Probe.extend({ 34 | 35 | probeClass: 'Repl', 36 | description: 'A socket.io based Read-Execute-Print-Loop console for node.js processes.', 37 | defaults: { 38 | // This assures output events are sent, even if the 39 | // data is the same as the prior output. 40 | sequence: 0, 41 | output: '' 42 | }, 43 | 44 | initialize: function(attributes, options){ 45 | var t = this; 46 | Probe.prototype.initialize.apply(t, arguments); 47 | 48 | // Don't send change events before connected 49 | process.nextTick(function(){ 50 | t.stream = new ReplStream(t); 51 | if (NEW_REPL) { 52 | t.repl = require('repl').start({ 53 | prompt: CONSOLE_PROMPT, 54 | input: t.stream, 55 | output: t.stream 56 | }); 57 | } else { 58 | t.repl = REPL.start(CONSOLE_PROMPT, t.stream); 59 | } 60 | t.htmlConsole = new HtmlConsole(t); 61 | t.shellCmd = null; 62 | t.repl.context.console = t.htmlConsole; 63 | }); 64 | }, 65 | 66 | /** 67 | * Send output to the terminal 68 | * 69 | * This forces the change event even if the last output is the same 70 | * as this output. 71 | * 72 | * @protected 73 | * @method output 74 | * @param str {String} String to output to the repl console 75 | */ 76 | _output: function(str) { 77 | var t = this; 78 | t.set({ 79 | output: str, 80 | sequence: t.get('sequence') + 1 81 | }); 82 | }, 83 | 84 | /** 85 | * Release any resources consumed by this probe. 86 | * 87 | * Stop the REPL console. Consoles live 1-1 with a UI counterpart, so stop 88 | * requests exit the underlying repl console. If the probe is re-started it 89 | * will get a new repl stream and console. 90 | * 91 | * @method release 92 | */ 93 | release: function(){ 94 | var t = this; 95 | t.stream = null; 96 | t.repl = null; 97 | }, 98 | 99 | /** 100 | * Process an autocomplete request from the client 101 | * 102 | * @method autocomplete 103 | * @param {Object} params Named parameters 104 | * @param {Function(error, returnParams)} callback Callback function 105 | */ 106 | autocomplete_control: function(params, callback) { 107 | var t = this; 108 | if (typeof(params) !== 'string' || params.length < 1) { 109 | callback("Autocomplete paramter must be a nonzero string"); 110 | } 111 | 112 | // Forward to the completion mechanism if it can be completed 113 | if (params.substr(-1).match(/([0-9])|([a-z])|([A-Z])|([_])/)) { 114 | t.repl.complete(params, callback); 115 | } else { 116 | // Return a no-op autocomplete 117 | callback(null, [[],'']); 118 | } 119 | }, 120 | 121 | /** 122 | * Handle user input from the console line 123 | * 124 | * @method input 125 | * @param {Object} params Named parameters 126 | * @param {Function(error, returnParams)} callback Callback function 127 | */ 128 | input_control: function(params, callback) { 129 | var t = this; 130 | if (params === '.break' && t.shellCmd) { 131 | t.shellCmd.kill(); 132 | } 133 | if (NEW_REPL) { 134 | t.stream.emit('data', params + "\n"); 135 | } else { 136 | t.stream.emit('data', params); 137 | } 138 | return callback(null); 139 | }, 140 | 141 | /** 142 | * Execute a shell command 143 | * 144 | * @method sh 145 | * @param {Object} params Named parameters 146 | * @param {Function(error, returnParams)} callback Callback function 147 | */ 148 | sh_control: function(params, callback) { 149 | var t = this; 150 | return callback(null, t._runShellCmd(params)); 151 | }, 152 | 153 | /** 154 | * Run a shell command and emit the output to the browser. 155 | * 156 | * @private 157 | * @method _runShellCmd 158 | * @param {String} command - The shell command to invoke 159 | */ 160 | _runShellCmd: function(command) { 161 | var t = this; 162 | t.shellCmd = ChildProcess.exec(command, function(err, stdout, stderr) { 163 | if (err) { 164 | var outstr = 'exit'; 165 | if (err.code) { 166 | outstr += ' (' + err.code + ')'; 167 | } 168 | if (err.signal) { 169 | outstr += ' ' + err.signal; 170 | } 171 | t._output(outstr); 172 | return null; 173 | } 174 | if (stdout.length) { 175 | t._output(stdout); 176 | } 177 | if (stderr.length) { 178 | t._output(stderr); 179 | } 180 | t.shellCmd = null; 181 | t._output(CONSOLE_PROMPT); 182 | }); 183 | return null; 184 | } 185 | 186 | }); 187 | 188 | // Define an internal stream class for the probe 189 | var ReplStream = function(probe){ 190 | var t = this; 191 | t.probe = probe; 192 | events.EventEmitter.call(t); 193 | if (t.setEncoding) { 194 | t.setEncoding('utf8'); 195 | } 196 | }; 197 | util.inherits(ReplStream, events.EventEmitter); 198 | // util.inherits(ReplStream, require('stream')); 199 | ReplStream.prototype.readable = true; 200 | ReplStream.prototype.writable = true; 201 | ['pause','resume','destroySoon','pipe', 'end'] 202 | .forEach(function(fnName){ 203 | ReplStream.prototype[fnName] = function(){ 204 | console.log("REPL Stream function unexpected: " + fnName); 205 | }; 206 | }); 207 | ['resume'] 208 | .forEach(function(fnName){ 209 | ReplStream.prototype[fnName] = function(){ 210 | // Handled 211 | }; 212 | }); 213 | ReplStream.prototype.write = function(data) { 214 | var t = this; 215 | t.probe._output(data); 216 | }; 217 | ReplStream.prototype.destroy = function(data) { 218 | var t = this; 219 | console.log("REPL stream destroy " + t.probe.get('id')); 220 | t.probe.stop(); 221 | }; 222 | 223 | // Define format if it's not in util. 224 | var formatRegExp = /%[sdj]/g; 225 | var format = util.format || function (f) { 226 | if (typeof f !== 'string') { 227 | var objects = []; 228 | for (var i = 0; i < arguments.length; i++) { 229 | objects.push(util.inspect(arguments[i])); 230 | } 231 | return objects.join(' '); 232 | } 233 | var j = 1; 234 | var args = arguments; 235 | var str = String(f).replace(formatRegExp, function(x) { 236 | switch (x) { 237 | case '%s': return String(args[j++]); 238 | case '%d': return Number(args[j++]); 239 | case '%j': return JSON.stringify(args[j++]); 240 | default: 241 | return x; 242 | } 243 | }); 244 | for (var len = args.length, x = args[j]; j < len; x = args[++j]) { 245 | if (x === null || typeof x !== 'object') { 246 | str += ' ' + x; 247 | } else { 248 | str += ' ' + util.inspect(x); 249 | } 250 | } 251 | return str; 252 | }; 253 | 254 | // Re-define the console so it goes to the HTML window 255 | var HtmlConsole = function(probe){ 256 | this.probe = probe; 257 | }; 258 | HtmlConsole.prototype.log = function(msg) { 259 | this.probe._output(format.apply(this, arguments)); 260 | }; 261 | HtmlConsole.prototype.info = HtmlConsole.prototype.log; 262 | HtmlConsole.prototype.warn = HtmlConsole.prototype.log; 263 | HtmlConsole.prototype.error = HtmlConsole.prototype.log; 264 | HtmlConsole.prototype.dir = function(object) { 265 | this.probe._output(util.inspect(object)); 266 | }; 267 | var times = {}; 268 | HtmlConsole.prototype.time = function(label) { 269 | times[label] = Date.now(); 270 | }; 271 | HtmlConsole.prototype.timeEnd = function(label) { 272 | var duration = Date.now() - times[label]; 273 | this.log('%s: %dms', label, duration); 274 | }; 275 | 276 | }(this)); 277 | -------------------------------------------------------------------------------- /lib/probes/StatProbe.js: -------------------------------------------------------------------------------- 1 | // StatProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root) { 6 | 7 | // Module loading - this runs server-side only 8 | var Monitor = root.Monitor || require('../Monitor'), 9 | _ = Monitor._, 10 | StreamProbe = Monitor.StreamProbe, 11 | Stat = Monitor.Stat; 12 | 13 | // Constants 14 | var DEFAULT_PATTERN = '*'; 15 | 16 | /** 17 | * Remote application statistics monitoring 18 | * 19 | * This probe forwards application statistics to the monitor. 20 | * 21 | * @class StatProbe 22 | * @extends StreamProbe 23 | * @constructor 24 | * @param [initParams] {Object} Probe initialization parameters 25 | * @param [initParams.pattern=*] {String} Stat name pattern to monitor (see Stat) 26 | * @param [initParams.interval=1000] {Numeric} Queue interval (see StreamProbe) 27 | * @param model {Object} Monitor data model elements 28 | * @param model.bundle {Stat array} Array of Stat elements. 29 | * @param model.bundle.timestamp {String} Timestamp of the stat entry 30 | * @param model.bundle.module {String} Stat module 31 | * @param model.bundle.name {String} Stat name 32 | * @param model.bundle.value {Numeric} Stat value 33 | * @param model.bundle.type {String} 'c'ounter, 'g'ague, or 'ms'timer 34 | * @param model.sequence {Integer} A numeric incrementer causing a change event 35 | */ 36 | var StatProbe = Monitor.StatProbe = StreamProbe.extend({ 37 | 38 | probeClass: 'Stat', 39 | 40 | defaults: _.extend({}, StreamProbe.prototype.defaults, { 41 | pattern: DEFAULT_PATTERN 42 | }), 43 | 44 | initialize: function(){ 45 | var t = this; 46 | 47 | // Call parent constructor 48 | StreamProbe.prototype.initialize.apply(t, arguments); 49 | 50 | // The watcher just forwards all args to queueItem as an array 51 | t.watcher = function() { 52 | // Add timestamp as the first element 53 | var logElems = _.toArray(arguments); 54 | logElems.splice(0,0,JSON.stringify(new Date()).substr(1,24)); 55 | t.queueItem.call(t, logElems); 56 | }; 57 | Stat.on(t.get('pattern'), t.watcher); 58 | }, 59 | 60 | release: function() { 61 | var t = this; 62 | Stat.off(t.get('pattern'), t.watcher); 63 | } 64 | 65 | }); 66 | 67 | }(this)); 68 | -------------------------------------------------------------------------------- /lib/probes/StreamProbe.js: -------------------------------------------------------------------------------- 1 | // StreamProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // Module loading 8 | var Monitor = root.Monitor || require('../Monitor'), 9 | Probe = Monitor.Probe, 10 | _ = Monitor._; 11 | 12 | // Constants 13 | var DEFAULT_BUNDLE_INTERVAL = 1000; 14 | 15 | /** 16 | * Base class for probes that stream data 17 | * 18 | * Offering real time data streaming can result in degraded performance due 19 | * to the I/O overhead of sending individual stream elements to remote monitors. 20 | * 21 | * This class eases that overhead by bundling stream elements, and sending those 22 | * bundles in scheduled intervals. The monitor gets to decide the interval based 23 | * on the stream volume, and their needs. 24 | * 25 | * Derived classes output their stream data as elements of the ```bundle``` 26 | * attribute. 27 | * 28 | * A ```sequence``` attribute is incremented sequentially to assure change 29 | * events are fired, and to allow clients to insure stream ordering and 30 | * completeness. 31 | * 32 | * @class StreamProbe 33 | * @extends Probe 34 | * @constructor 35 | * @param [initParams] {Object} Probe initialization parameters 36 | * @param [initParams.interval=1000] {Numeric} Number of milliseconds 37 | * to wait between bundles. 38 | */ 39 | var StreamProbe = Monitor.StreamProbe = Probe.extend({ 40 | 41 | 42 | defaults: _.extend({}, Probe.prototype.defaults, { 43 | bundle: [], 44 | interval: DEFAULT_BUNDLE_INTERVAL, 45 | sequence: 0 46 | }), 47 | 48 | initialize: function(){ 49 | var t = this; 50 | 51 | // Initialize parent 52 | Probe.prototype.initialize.apply(t, arguments); 53 | 54 | // Moving the interval into an instance variable for performance 55 | t.interval = t.get('interval'); 56 | 57 | // Set up for the first bundle 58 | t.queue = []; 59 | t.timer = null; 60 | t.lastSendTime = 0; 61 | }, 62 | 63 | /** 64 | * Queue an item in the stream 65 | * 66 | * This method places the item into the stream and outputs it to the 67 | * monitor, or queues it up for the next bundle. 68 | * 69 | * @method queueItem 70 | * @param item {Any} Item to place into the queue 71 | */ 72 | queueItem: function(item) { 73 | var t = this, 74 | now = Date.now(), 75 | msSinceLastSend = now - t.lastSendTime; 76 | 77 | // Queue the item 78 | t.queue.push(item); 79 | 80 | // Send the bundle? 81 | if (msSinceLastSend > t.interval) { 82 | // It's been a while since the last send. Send it now. 83 | t._send(); 84 | } 85 | else { 86 | // Start the timer if it's not already running 87 | if (!t.timer) { 88 | t.timer = setTimeout(function(){ 89 | t._send(); 90 | }, t.interval - msSinceLastSend); 91 | } 92 | } 93 | }, 94 | 95 | /** 96 | * Send the bundle to the montitor 97 | * 98 | * @private 99 | * @method _send 100 | */ 101 | _send: function() { 102 | var t = this, 103 | now = Date.now(); 104 | 105 | // This kicks off the send 106 | t.lastSendTime = now; 107 | t.set({ 108 | bundle: t.queue, 109 | sequence: t.get('sequence') + 1 110 | }); 111 | 112 | // Reset 113 | t.queue = []; 114 | if (t.timer) { 115 | clearTimeout(t.timer); 116 | t.timer = null; 117 | } 118 | } 119 | 120 | }); 121 | 122 | }(this)); 123 | -------------------------------------------------------------------------------- /lib/probes/SyncProbe.js: -------------------------------------------------------------------------------- 1 | // SyncProbe.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // Module loading - this runs server-side only 8 | var Monitor = root.Monitor || require('../Monitor'), 9 | _ = Monitor._, Probe = Monitor.Probe; 10 | 11 | /** 12 | * Probe for exposing backbone data models from server-side persistence 13 | * 14 | * This probe is used by the client-side Sync class 15 | * to connect a local backbone model with server-side storage. 16 | * 17 | * It delegates to a specialized SyncProbe defined by the server for the 18 | * specific data class. For example, the server may determine that one class 19 | * type uses FileSyncProbe, and another class uses a different persistence 20 | * mechanism. 21 | * 22 | * For security purposes, the server must configure specific SyncProbes for 23 | * classes, or a default sync probe before this will operate. 24 | * 25 | * @class SyncProbe 26 | * @extends Probe 27 | * @constructor 28 | * @param className {String} Name of the class to synchronize with 29 | * @param [modelId] {String} Id of the data model for live synchronization 30 | * If not set, a non-live probe is set up for control access only. 31 | * @param [model] {Object} If this is a liveSync probe, this contains 32 | * the attributes of the current model object. 33 | */ 34 | var SyncProbe = Monitor.SyncProbe = Probe.extend({ 35 | 36 | probeClass: 'Sync', 37 | defaults: { 38 | className: null, 39 | modelId: null, 40 | model: null 41 | }, 42 | 43 | initialize: function(attributes, options){ 44 | var t = this; 45 | Probe.prototype.initialize.apply(t, arguments); 46 | 47 | // Determine the probe name based on the class, and coerce this 48 | // object into one of those by copying all prototype methods. 49 | var className = t.get('className'), 50 | config = SyncProbe.Config, 51 | probeClassName = config.classMap[className] || config.defaultProbe, 52 | probeClass = SyncProbe[probeClassName]; 53 | _.each(_.functions(probeClass.prototype), function(methodName) { 54 | t[methodName] = probeClass.prototype[methodName]; 55 | }); 56 | t.probeClass = probeClass.prototype.probeClass; 57 | 58 | // Forward class initialization to the coerced initialize method 59 | return t.initialize.apply(t, arguments); 60 | }, 61 | 62 | release: function() { 63 | var t = this; 64 | Probe.prototype.release.apply(t, arguments); 65 | }, 66 | 67 | /** 68 | * Create and save a new instance of the class into storage 69 | * 70 | * This probe control requests a new instance of a data model to be 71 | * persisted onto storage. It is invoked when a data model that has 72 | * the Sync probe attached calls ```save()``` on a new object. 73 | * 74 | * @method create_control 75 | * @param model {Object} Full data model to save. This must contain 76 | * the id element. 77 | * @param callback {Function(error, result)} Callback when complete 78 | * @param callback.error {Mixed} Set if an error occurs during creation. 79 | * @param callback.result {Object} An object containing any differing 80 | * parameters from the model sent in. Normally a blank object. 81 | */ 82 | create_control: function(args, callback) { 83 | callback({msg: 'not implemented'}); 84 | }, 85 | 86 | /** 87 | * Read an instance from storage 88 | * 89 | * This probe control reads the instance with the specified id 90 | * from storage, and returns it in the callback. 91 | * 92 | * @method read_control 93 | * @param id {String} ID of the object to read 94 | * @param callback {Function(error, result)} Callback when complete 95 | * @param callback.error {Mixed} Set if an error occurs during read. 96 | * if error.code === 'NOTFOUND' then the requested object wasn't found. 97 | * if error.code === 'PARSE' then the document was poorly formatted JSON. 98 | * @param callback.result {Object} The full object. 99 | */ 100 | read_control: function(args, callback) { 101 | callback({msg: 'not implemented'}); 102 | }, 103 | 104 | /** 105 | * Update a data model in storage 106 | * 107 | * This acts like a REST PUT, meaning it can create a new object, or 108 | * update an existing object. 109 | * 110 | * Backbone has only a save() method. If the client sets the ID 111 | * of the object before save(), Backbone thinks the object exists and 112 | * will call update vs. create. 113 | * 114 | * @method update_control 115 | * @param model {Object} Full data model to save. This must contain 116 | * the id element. 117 | * @param callback {Function(error, result)} Callback when complete 118 | * @param callback.error {Mixed} Set if an error occurs during save. 119 | * @param callback.result {Object} An object containing any differing 120 | * parameters from the model sent in. Normally a blank object. 121 | */ 122 | update_control: function(args, callback) { 123 | callback({msg: 'not implemented'}); 124 | }, 125 | 126 | /** 127 | * Delete an instance from storage 128 | * 129 | * This probe control deletes the instance with the specified id 130 | * from storage. 131 | * 132 | * @method delete_control 133 | * @param id {String} ID of the object to read 134 | * @param callback {Function(error)} Callback when complete 135 | * @param callback.error {Mixed} Set if an error occurs during read. 136 | */ 137 | delete_control: function(args, callback) { 138 | callback({msg: 'not implemented'}); 139 | } 140 | 141 | }); 142 | 143 | /** 144 | * Static Configurations 145 | * 146 | * These can be set onto the Monitor.SyncProbe class after it's loaded. 147 | * 148 | * The SyncProbe will *not* work until the defaultProbe is defined. 149 | * 150 | * Example: 151 | * 152 | * var syncConfig = Monitor.SyncProbe.Config; 153 | * syncConfig.defaultProbe = 'FileSyncProbe'; 154 | * syncConfig.classMap = { 155 | * Book: 'MongoDbSync', 156 | * Author: 'MongoDbSync' 157 | * } 158 | * 159 | * @static 160 | * @property Config 161 | * @type <Object> 162 | *
    163 | *
  • defaultProbe (String) Name of the sync probe to use if the class isn't listed in the classMap
  • 164 | *
  • classMap (Object) Map of className to sync probe name to use instead of the default for that class
  • 165 | *
166 | */ 167 | var defaultConfig = { 168 | defaultProbe: '', 169 | classMap: {} 170 | }; 171 | 172 | // Expose default configurations to the config package 173 | SyncProbe.Config = _.extend({}, defaultConfig); 174 | 175 | }(this)); 176 | -------------------------------------------------------------------------------- /monitor.js: -------------------------------------------------------------------------------- 1 | // monitor.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // Load dependencies 8 | var Monitor = require('./lib/index'), 9 | log = Monitor.getLogger('monitor'), 10 | stat = Monitor.getStatLogger('monitor'), 11 | OS = require('os'); 12 | 13 | /** 14 | * Bootstrap for a standalone monitor server 15 | * 16 | * @static 17 | * @class server 18 | */ 19 | 20 | console.log(""); 21 | console.log(" __________"); 22 | console.log("_______ ___________________(_)_ /______________ "); 23 | console.log("__ __ `__ \\ __ \\_ __ \\_ /_ __/ __ \\_ ___/"); 24 | console.log("_ / / / / / /_/ / / / / / / /_ / /_/ / /"); 25 | console.log("/_/ /_/ /_/\\____//_/ /_//_/ \\__/ \\____//_/"); 26 | console.log(""); 27 | 28 | // Boot the monitor server. 29 | // This accepts websocket connections on the configured port. 30 | var server = new Monitor.Server(); 31 | server.start(function(error) { 32 | if (error) { 33 | log.error('monitor.start', error); 34 | return; 35 | } 36 | 37 | var connectTo = Monitor.Config.Monitor.allowExternalConnections ? OS.hostname() : 'localhost'; 38 | console.log('Monitor service started on host: ' + connectTo); 39 | 40 | // Output security concerns 41 | if (!Monitor.Config.Monitor.allowExternalConnections) { 42 | console.log(""); 43 | console.log("External connections disabled."); 44 | console.log("See " + process.cwd() + "/config/external.js for more information."); 45 | } 46 | 47 | }); 48 | 49 | // Process uncaught exceptions. 50 | process.on('uncaughtException', function(err){ 51 | 52 | // On laptop sleep/startup the DNS servers aren't immediately available, 53 | // resulting in a flood of these for socket.io until DNS services are back up. 54 | if (err.message === 'ECONNREFUSED, Could not contact DNS servers') { 55 | return; 56 | } 57 | 58 | // Don't allow the process to continue in an unknown state. 59 | log.fatal('moniotor.uncaught', 'Uncaught Exception: ' + err.message); 60 | log.fatal('moniotor.uncaught', err.stack); 61 | server.stop(function(){ 62 | process.exit(1); 63 | }); 64 | 65 | // Don't wait around if the server is hung. 66 | setTimeout(function(){process.exit(1);}, 2000); 67 | }); 68 | 69 | }(this)); 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monitor", 3 | "description": "Runtime monitoring for node.js applications", 4 | "version": "0.6.10", 5 | "main": "./lib/index.js", 6 | "author": { 7 | "name": "Loren West", 8 | "email": "open_source@lorenwest.com", 9 | "url": "https://github.com/lorenwest" 10 | }, 11 | "homepage": "http://lorenwest.github.com/node-monitor/", 12 | "keywords": ["monitor", "node-monitor", "remote control", "realtime", "probe", "JMX"], 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/lorenwest/node-monitor.git" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/lorenwest/node-monitor/blob/master/LICENSE" 21 | } 22 | ], 23 | "dependencies": { 24 | "config": ">=0.4.34 <0.5.0", 25 | "cron": ">=0.1.3 <0.2.0", 26 | "backbone": "0.9.9", 27 | "underscore": ">=1.4.3 <1.5.0", 28 | "backbone-callbacks": ">=0.1.4 <0.2.0", 29 | "socket.io-client": ">=0.9.11 <0.10.0", 30 | "socket.io": ">=0.9.10 <0.10.0" 31 | }, 32 | "devDependencies": { 33 | "grunt": "0.3.17" 34 | }, 35 | "engines": { "node": ">= 0.6.0" }, 36 | "scripts": { 37 | "test": "grunt test", 38 | "start": "node monitor" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/ConnectionTest.js: -------------------------------------------------------------------------------- 1 | // ConnectionTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | Connection = Monitor.Connection, Backbone = Monitor.Backbone, 14 | server, serverPort; 15 | 16 | /** 17 | * Unit tests for the Connection class. 18 | * @class ConnectionTest 19 | */ 20 | 21 | /** 22 | * Test group for connection functionality 23 | * @method Connection 24 | */ 25 | module.exports['Connection'] = { 26 | 27 | /** 28 | * Create a Server to test connections with 29 | * @method Connection-setUp 30 | */ 31 | setUp: function(callback) { 32 | server = new Monitor.Server(); 33 | server.start(callback); 34 | }, 35 | 36 | /** 37 | * Tests that the Connection classes are available 38 | * @method Connection-Classes 39 | */ 40 | Classes: function(test) { 41 | test.ok(Connection.prototype instanceof Backbone.Model, 'The Connection data model is in place'); 42 | test.ok(Connection.List.prototype instanceof Backbone.Collection, 'The Connection.List collection is in place'); 43 | test.done(); 44 | }, 45 | 46 | /** 47 | * Assure that a connect / disconnect to the server host/port works 48 | * @method Connection-ConnectDisconnect 49 | */ 50 | ConnectDisconnect: function(test) { 51 | var port = server.get('port'), conn = new Monitor.Connection({hostName:'localhost', hostPort:port}); 52 | conn.on('connect', function() { 53 | test.ok(conn.get('remoteHostName'), 'The remote host name is known'); 54 | conn.on('disconnect', test.done); 55 | conn.disconnect(); 56 | }); 57 | }, 58 | 59 | /** 60 | * Test pinging the remote connection 61 | * @method Connection-PingPong 62 | */ 63 | PingPong: function(test) { 64 | var port = server.get('port'), conn = new Monitor.Connection({hostName:'localhost', hostPort:port}); 65 | conn.on('connect', function() { 66 | test.ok(conn.get('remoteHostName'), 'The remote host name is known'); 67 | conn.ping(function(){ 68 | test.ok(true, 'Ping made its way to and from the remote server'); 69 | conn.on('disconnect', test.done); 70 | conn.disconnect(); 71 | }); 72 | }); 73 | }, 74 | 75 | /** 76 | * Tear down the test Server 77 | * @method Connection-tearDown 78 | */ 79 | tearDown: function(callback) { 80 | server.stop(callback); 81 | } 82 | 83 | }; 84 | 85 | }(this)); 86 | -------------------------------------------------------------------------------- /test/DataModelProbeTest.js: -------------------------------------------------------------------------------- 1 | // DataModelProbeTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | DataModelProbe = Monitor.DataModelProbe, 14 | dataModelMonitor = null, 15 | dataModelMonitor1 = null, 16 | dataModelMonitor2 = null, 17 | Backbone = Monitor.Backbone, _ = Monitor._; 18 | 19 | /** 20 | * Unit tests for the DataModel probe. 21 | * @class DataModelProbeTest 22 | */ 23 | 24 | /** 25 | * Test group for baseline DataModel probe functionality 26 | * 27 | * @method DataModelProbe 28 | */ 29 | module.exports['DataModelProbe'] = { 30 | 31 | /** 32 | * Tests that classes are in correct 33 | * @method DataModelProbe-Classes 34 | */ 35 | Classes: function(test) { 36 | test.ok(DataModelProbe.prototype instanceof Backbone.Model, 'The data model is in place'); 37 | test.ok(DataModelProbe.prototype instanceof Monitor.Probe, 'It is a probe'); 38 | test.done(); 39 | }, 40 | 41 | /** 42 | * Test that you can connect to the test data model 43 | * @method DataModelProbe-Connect 44 | */ 45 | Connect: function(test) { 46 | // Get a monitor to the underlying data model the recipe manipulates 47 | setTimeout(function(){ 48 | dataModelMonitor = new Monitor({probeName:'DataModelTest'}); 49 | dataModelMonitor.connect(function(error) { 50 | test.ok(!error, 'No error on data model connect'); 51 | test.equal(dataModelMonitor.get('testParam1'), 'testValue1', 'Connected to the correct data model'); 52 | test.done(); 53 | }); 54 | }, 0); 55 | }, 56 | 57 | /** 58 | * Tests that a single monitor can update the value 59 | * @method DataModelProbe-SingleMonitor 60 | */ 61 | SingleMonitor: function(test) { 62 | 63 | // Expect only a single change (vs. a change in the monitor, and a subsequent 64 | // change from the probe). 65 | var onChange = function() { 66 | test.equal(dataModelMonitor.get('testParam1'), 'testValue2', 'The change event was triggered correctly'); 67 | }; 68 | 69 | // Done with the test in 10 ms (should have only gotten one onChange event triggered) 70 | setTimeout(function(){ 71 | dataModelMonitor.off('change:testParam1', onChange); 72 | test.expect(1); 73 | test.done(); 74 | }, 10); 75 | 76 | // Now attach the listener and change an attribute 77 | dataModelMonitor.on('change:testParam1', onChange); 78 | dataModelMonitor.set('testParam1', 'testValue2'); 79 | }, 80 | 81 | /** 82 | * Tests that multiple monitors can all be triggered from a single change 83 | * @method DataModelProbe-SingleMonitor 84 | */ 85 | MultiMonitor: function(test) { 86 | 87 | // Expect two changes - one from each monitor 88 | var onChange1 = function() { 89 | test.equal(dataModelMonitor.get('testParam1'), 'testValue3', 'The change event was triggered correctly'); 90 | dataModelMonitor1.off('change:testParam1', onChange1); 91 | }; 92 | var onChange2 = function() { 93 | test.equal(dataModelMonitor.get('testParam1'), 'testValue3', 'The change event was triggered correctly'); 94 | dataModelMonitor2.off('change:testParam1', onChange2); 95 | }; 96 | 97 | dataModelMonitor1 = new Monitor({probeName:'DataModelTest'}); 98 | dataModelMonitor2 = new Monitor({probeName:'DataModelTest'}); 99 | 100 | dataModelMonitor1.connect(function(error) { 101 | test.ok(!error, 'No error on data model 1 connect'); 102 | test.equal(dataModelMonitor.get('testParam1'), 'testValue2', 'Connected to the correct data model'); 103 | dataModelMonitor1.on('change:testParam1', onChange1); 104 | 105 | dataModelMonitor2.connect(function(error) { 106 | test.ok(!error, 'No error on data model 2 connect'); 107 | test.equal(dataModelMonitor.get('testParam1'), 'testValue2', 'Connected to the correct data model'); 108 | dataModelMonitor2.on('change:testParam1', onChange2); 109 | 110 | // Now change the value from the original monitor. This change should 111 | // be propagated to the two other monitors just connected. 112 | dataModelMonitor.set('testParam1', 'testValue3'); 113 | 114 | // Done with the test in 10 ms (should have gotten two onChange events triggered 115 | // from different monitors, plus the 4 connect tests) 116 | setTimeout(function(){ 117 | test.expect(6); 118 | test.done(); 119 | }, 10); 120 | 121 | }); 122 | }); 123 | 124 | } 125 | 126 | }; 127 | 128 | }(this)); 129 | -------------------------------------------------------------------------------- /test/FileProbeTest.js: -------------------------------------------------------------------------------- 1 | // FileProbeTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | Path = require('path'), 14 | FS = require('fs'), 15 | FileProbe = Monitor.FileProbe, 16 | Backbone = Monitor.Backbone, _ = Monitor._; 17 | 18 | // Constants 19 | var TEST_ROOT_PATH = __dirname + '/fileTestData', 20 | TEST_FILE_RELATIVE_PATH = 'testdir/testfile1.json', 21 | TEST_FILE_FULL_PATH = Path.join(TEST_ROOT_PATH, TEST_FILE_RELATIVE_PATH), 22 | TEST_FILE_DIR = Path.dirname(TEST_FILE_FULL_PATH), 23 | TEST_OBJECT = { 24 | testNumber:1, 25 | testString:"two", 26 | testObject:{some:"sub_object"}, 27 | testArray:[1, "two", 3] 28 | }, 29 | JSON_CONTENT = JSON.stringify(TEST_OBJECT, null, 2); 30 | 31 | // Old style watch takes *forever* to connect 32 | var OLD_WATCHER_CONNECT_MS = 1000, 33 | NEW_WATCHER_CONNECT_MS = 10, 34 | WATCH_CONNECT_TIME = FS.watch ? NEW_WATCHER_CONNECT_MS : OLD_WATCHER_CONNECT_MS; 35 | 36 | /** 37 | * Unit tests for the File probe. 38 | * @class FileProbeTest 39 | */ 40 | 41 | /** 42 | * Test group for baseline FileProbe functionality 43 | * 44 | * @method FileProbe 45 | */ 46 | module.exports['FileProbe'] = { 47 | 48 | /** 49 | * Tests that classes are in correct 50 | * @method FileProbe-Classes 51 | */ 52 | Classes: function(test) { 53 | test.ok(FileProbe.prototype instanceof Backbone.Model, 'The data model is in place'); 54 | test.ok(FileProbe.prototype instanceof Monitor.Probe, 'It is a probe'); 55 | test.done(); 56 | }, 57 | 58 | /** 59 | * Tests that public static methods are in place 60 | * @method FileProbe-Static 61 | */ 62 | Static: function(test) { 63 | test.equal(typeof FileProbe.setRootPath, 'function'); 64 | test.equal(typeof FileProbe.getRootPath, 'function'); 65 | test.equal(typeof FileProbe.rm_rf, 'function'); 66 | test.equal(typeof FileProbe.mkdir_r, 'function'); 67 | test.equal(typeof FileProbe.watch, 'function'); 68 | test.equal(typeof FileProbe.watchLoad, 'function'); 69 | test.equal(typeof FileProbe.tail, 'function'); 70 | test.done(); 71 | } 72 | 73 | }; 74 | 75 | /** 76 | * Test group for static file/directory utilities 77 | * 78 | * @method Utils 79 | */ 80 | module.exports['Utils'] = { 81 | 82 | /** 83 | * Test the mkdir_r utility 84 | * @method Utils-Mkdir_R 85 | */ 86 | Mkdir_R: function(test) { 87 | FileProbe.mkdir_r(TEST_FILE_DIR, function(error) { 88 | test.ok(!error, 'Mkdir-r error ' + JSON.stringify(error)); 89 | var status = FS.statSync(TEST_FILE_DIR); 90 | test.ok(status.isDirectory(), "Recursive mkdir created all directories"); 91 | test.done(); 92 | }); 93 | }, 94 | 95 | /** 96 | * Test the rm_rf utility 97 | * @method Utils-Rm_Rf 98 | */ 99 | Rm_Rf: function(test) { 100 | // Make a test directory structure 101 | FileProbe.mkdir_r(TEST_FILE_DIR, function(error) { 102 | var status = FS.statSync(TEST_FILE_DIR); 103 | test.ok(status.isDirectory(), "Recursive mkdir created all directories"); 104 | 105 | // Make a bunch of files in the lowest directory 106 | for (var i = 0; i < 10; i++) { 107 | FS.writeFileSync(TEST_FILE_FULL_PATH + i, JSON_CONTENT); 108 | } 109 | 110 | // Remove everything from the test root 111 | FileProbe.rm_rf(TEST_ROOT_PATH, function(err1) { 112 | test.ok(!err1, "rm_rf error: " + err1); 113 | FS.readdir(TEST_FILE_DIR, function(err2, files) { 114 | test.ok(err2, "Readdir correctly reported an error on no directory"); 115 | test.equal(err2.code, 'ENOENT', "Directory and all sub-files removed correctly"); 116 | test.done(); 117 | }); 118 | }); 119 | }); 120 | }, 121 | 122 | /** 123 | * Tests the file watch functionality 124 | * @method Utils-Watch 125 | */ 126 | Watch: function(test) { 127 | // Create a file 128 | FileProbe.mkdir_r(TEST_FILE_DIR, function(err) { 129 | test.ok(!err, 'Made the test directory'); 130 | writeTestFile(); 131 | 132 | // Get a watcher on the file 133 | var watcher = FileProbe.watch(TEST_FILE_FULL_PATH, {persistent:true}, function(){ 134 | test.ok(true, "File change detected"); 135 | watcher.close(); 136 | FileProbe.rm_rf(TEST_ROOT_PATH, function(){ 137 | test.done(); 138 | }); 139 | }); 140 | 141 | // Wait for the O/S to start watching, then change the file 142 | writeTestFile("new", WATCH_CONNECT_TIME); 143 | }); 144 | }, 145 | 146 | /** 147 | * Tests the polling style file watching mechanism 148 | * @method Utils-PollingWatcher 149 | */ 150 | PollingWatcher: function(test) { 151 | // Create a file 152 | FileProbe.mkdir_r(TEST_FILE_DIR, function(err) { 153 | test.ok(!err, 'Made the test directory'); 154 | writeTestFile(); 155 | 156 | // Test the old-style watching 157 | var watcher = FileProbe.watch(TEST_FILE_FULL_PATH, {persistent:true, pollStyle:true}, function(){ 158 | test.ok(true, "File change detected"); 159 | watcher.close(); 160 | FileProbe.rm_rf(TEST_ROOT_PATH, function(){ 161 | test.done(); 162 | }); 163 | }); 164 | 165 | // Wait long enough for the old-style watcher to connect 166 | writeTestFile('new', OLD_WATCHER_CONNECT_MS); 167 | }); 168 | }, 169 | 170 | /** 171 | * Tests the watchLoad functionality 172 | * @method Utils-WatchLoad 173 | */ 174 | WatchLoad: function(test) { 175 | // Create a file 176 | FileProbe.mkdir_r(TEST_FILE_DIR, function(err) { 177 | test.ok(!err, 'Made the test directory'); 178 | writeTestFile(); 179 | 180 | // Test with initial preload 181 | var watchCount = 0; 182 | var watcher = FileProbe.watchLoad(TEST_FILE_FULL_PATH, {persistent:true, preload:true}, function(err1, content){ 183 | test.ok(!err1, "watchLoad error: " + err1); 184 | watchCount++; 185 | if (watchCount === 1) { 186 | test.equal(content, JSON_CONTENT, "Initial file contents preloaded"); 187 | 188 | // Test with subsequent file write 189 | writeTestFile("extra", WATCH_CONNECT_TIME); 190 | } else if (watchCount === 2) { 191 | test.equal(content, JSON_CONTENT + "extra", "Subsequent content loaded after update"); 192 | watcher.close(); 193 | FileProbe.rm_rf(TEST_ROOT_PATH, function(){ 194 | test.done(); 195 | }); 196 | } 197 | }); 198 | }); 199 | } 200 | 201 | }; 202 | 203 | /** 204 | * Test group for File based probe functionality 205 | * 206 | * @method Probe 207 | */ 208 | module.exports['Probe'] = { 209 | 210 | // Create the test directory and file 211 | setUp: function(callback) { 212 | FileProbe.mkdir_r(TEST_FILE_DIR, function(err) { 213 | writeTestFile(); 214 | callback(); 215 | }); 216 | }, 217 | 218 | // Remove the test directory 219 | tearDown: function(callback) { 220 | FileProbe.rm_rf(TEST_ROOT_PATH, function(){ 221 | callback(); 222 | }); 223 | }, 224 | 225 | /** 226 | * Tests the ROOT_PATH functionality 227 | * @method Probe-RootPath 228 | */ 229 | RootPath: function(test) { 230 | // Only perform tests if root path hasn't been set 231 | if (FileProbe.getRootPath() === null) { 232 | FileProbe.setRootPath(TEST_ROOT_PATH); 233 | test.equal(FileProbe.getRootPath(), TEST_ROOT_PATH); 234 | try { 235 | FileProbe.setRootPath('/'); 236 | test.ok(false, "This line shouldn't be reached"); 237 | } 238 | catch (e) { 239 | test.ok(true, "Root path correctly disallowed changes"); 240 | } 241 | } 242 | test.done(); 243 | }, 244 | 245 | /** 246 | * This tests the File probe initializes properly 247 | * @method Probe-Init 248 | */ 249 | Init: function(test) { 250 | 251 | // Build and connect the file monitor 252 | var fileMonitor = new Monitor({ 253 | probeClass: "File", 254 | initParams: {path: TEST_FILE_RELATIVE_PATH} 255 | }); 256 | fileMonitor.connect(function(){ 257 | 258 | // Make sure the initial content is correct 259 | test.equal(fileMonitor.get("text"), JSON_CONTENT, "File content is correct"); 260 | 261 | // Watch for file changes after first change event 262 | process.nextTick(function(){ 263 | fileMonitor.on('change', function() { 264 | test.equal(fileMonitor.get("text"), JSON_CONTENT + 'altered', "Altered file content is correct"); 265 | fileMonitor.off('change'); 266 | fileMonitor.disconnect(); 267 | test.done(); 268 | }); 269 | }); 270 | 271 | // Alter the file 272 | writeTestFile('altered', WATCH_CONNECT_TIME); 273 | }); 274 | } 275 | }; 276 | 277 | /** 278 | * Write the test file to disk, with possible appendage 279 | * 280 | * @static 281 | * @method writeTestFile 282 | * @param append {String} String to append onto the standard test file 283 | * @param wait {Integer} Number of milliseconds to wait before writing 284 | */ 285 | function writeTestFile(append, wait) { 286 | var content = append ? JSON_CONTENT + append : JSON_CONTENT; 287 | if (wait) { 288 | setTimeout(function(){ 289 | FS.writeFileSync(TEST_FILE_FULL_PATH, content); 290 | }, wait); 291 | } else { 292 | FS.writeFileSync(TEST_FILE_FULL_PATH, content); 293 | } 294 | } 295 | 296 | }(this)); 297 | -------------------------------------------------------------------------------- /test/InspectTest.js: -------------------------------------------------------------------------------- 1 | // InspectTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | InspectProbe = Monitor.InspectProbe, 14 | Backbone = Monitor.Backbone, _ = Monitor._; 15 | 16 | /** 17 | * Unit tests for the Inspect probe. 18 | * @class InspectTest 19 | */ 20 | 21 | /** 22 | * Test group for baseline Inspect probe functionality 23 | * 24 | * @method Inspect 25 | */ 26 | module.exports['Inspect'] = { 27 | 28 | /** 29 | * Tests that classes are in correct 30 | * @method Inspect-Classes 31 | */ 32 | Classes: function(test) { 33 | test.ok(InspectProbe.prototype instanceof Backbone.Model, 'The data model is in place'); 34 | test.ok(InspectProbe.prototype instanceof Monitor.Probe, 'It is a probe'); 35 | test.ok(InspectProbe.prototype instanceof Monitor.PollingProbe, 'It is a polling probe'); 36 | test.done(); 37 | }, 38 | 39 | /** 40 | * Tests the no-param constructor 41 | * @method Inspect-NoParams 42 | */ 43 | NoParams: function(test) { 44 | var monitor = new Monitor({ 45 | probeClass:'Inspect' 46 | }); 47 | monitor.connect(function(error) { 48 | test.ok(!error, "Able to construct a top level inspector"); 49 | var globalValue = monitor.get('value'); 50 | test.ok(typeof globalValue.Monitor !== 'undefined', 'Global object returned'); 51 | monitor.disconnect(function(error){ 52 | test.ok(!error, 'Properly disconnected'); 53 | test.done(); 54 | }); 55 | }); 56 | }, 57 | 58 | /** 59 | * Tests the key parameter as a global variable 60 | * @method Inspect-KeyVariable 61 | */ 62 | KeyVariable: function(test) { 63 | var monitor = new Monitor({ 64 | probeClass:'Inspect', 65 | initParams: { 66 | key: 'Monitor' 67 | } 68 | }); 69 | monitor.connect(function(error) { 70 | test.ok(!error, "Able to inspect a global variable"); 71 | var value = monitor.get('value'); 72 | test.ok(typeof value.Probe !== 'undefined', 'The monitor object was returned'); 73 | monitor.disconnect(function(error){ 74 | test.ok(!error, 'Properly disconnected'); 75 | test.done(); 76 | }); 77 | }); 78 | }, 79 | 80 | /** 81 | * Tests the key parameter as an expression 82 | * @method Inspect-KeyExpression 83 | */ 84 | KeyExpression: function(test) { 85 | var monitor = new Monitor({ 86 | probeClass:'Inspect', 87 | initParams: { 88 | key: 'Monitor.getRouter()' 89 | } 90 | }); 91 | monitor.connect(function(error) { 92 | test.ok(!error, "Able to inspect an expression"); 93 | var value = monitor.get('value'); 94 | test.ok(typeof value.firewall !== 'undefined', 'The expression returned the correct object'); 95 | monitor.disconnect(function(error){ 96 | test.ok(!error, 'Properly disconnected'); 97 | test.done(); 98 | }); 99 | }); 100 | } 101 | 102 | }; 103 | 104 | }(this)); 105 | -------------------------------------------------------------------------------- /test/LogTest.js: -------------------------------------------------------------------------------- 1 | // LogTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | Log = Monitor.Log, 14 | log = Monitor.getLogger('log-test'), 15 | Backbone = Monitor.Backbone; 16 | 17 | /** 18 | * Unit tests for the Log class. 19 | * @class LogTest 20 | */ 21 | 22 | /** 23 | * Test group for baseline Log functionality 24 | * 25 | * @method Log 26 | */ 27 | module.exports['Log'] = { 28 | 29 | setUp: function(callback) {callback();}, 30 | tearDown: function(callback) {callback();}, 31 | 32 | /** 33 | * Tests that Log class is in place 34 | * @method Log-Classes 35 | */ 36 | Classes: function(test) { 37 | test.ok(Log.prototype, 'The Log model is in place'); 38 | test.done(); 39 | }, 40 | 41 | /** 42 | * Tests that registering for '*' emits all logs 43 | * @method Log-AllLogs 44 | */ 45 | AllLogs: function(test) { 46 | 47 | // Listen for all logs 48 | Log.once('*', function(log, module, name) { 49 | test.equals(log, 'info', 'The log type is correct'); 50 | test.equals(module, 'log-test', 'found the log-test module when listening to *'); 51 | test.equals(name, 'All logs test', 'found the correct log name *'); 52 | test.done(); 53 | }); 54 | log.info('All logs test', 34); 55 | }, 56 | 57 | /** 58 | * Tests for the inclusion of multiple arguments in logs 59 | * @method Log-MultiArg 60 | */ 61 | MultiArg: function(test) { 62 | 63 | // Listen for all logs 64 | Log.once('*', function(log, module, name, hello, number, obj) { 65 | test.equals(log, 'error', 'The log type is correct'); 66 | test.equals(name, 'MultiArg', 'found the correct log name *'); 67 | test.equals(hello, 'hello', 'hello arg is found'); 68 | test.equals(number, 34, 'Numeric argument is found'); 69 | test.ok(obj, 'Object argument is found'); 70 | test.equals(obj.there, 'world', 'Object elements are intact'); 71 | test.equals(obj.num, 9273, 'Object elements are intact'); 72 | test.done(); 73 | }); 74 | log.error('MultiArg', 'hello', 34, {there:'world', num: 9273}); 75 | }, 76 | 77 | /** 78 | * Tests for the trace log 79 | * @method Log-trace 80 | */ 81 | TraceLog: function(test) { 82 | 83 | // Listen for trace logs 84 | Log.once('trace.*', function(log, module, name) { 85 | test.equals(log, 'trace', 'The log type is correct'); 86 | test.equals(module, 'log-test', 'found the correct module name'); 87 | test.equals(name, 'traceLog', 'found the correct log name'); 88 | test.done(); 89 | }); 90 | log.trace('traceLog'); 91 | }, 92 | 93 | /** 94 | * Tests for the debug log 95 | * @method Log-debug 96 | */ 97 | DebugLog: function(test) { 98 | 99 | // Listen for debug logs 100 | Log.once('*.*.debugLog', function(log, module, name) { 101 | test.equals(log, 'debug', 'The log type is correct'); 102 | test.equals(module, 'log-test', 'found the correct module name'); 103 | test.equals(name, 'debugLog', 'found the correct log name'); 104 | test.done(); 105 | }); 106 | log.debug('debugLog'); 107 | }, 108 | 109 | /** 110 | * Tests for the info log 111 | * @method Log-info 112 | */ 113 | InfoLog: function(test) { 114 | 115 | // Listen for info logs 116 | Log.once('*.log-test.infoLog', function(log, module, name) { 117 | test.equals(log, 'info', 'The log type is correct'); 118 | test.equals(module, 'log-test', 'found the correct module name'); 119 | test.equals(name, 'infoLog', 'found the correct log name'); 120 | test.done(); 121 | }); 122 | log.info('infoLog'); 123 | }, 124 | 125 | /** 126 | * Tests for the warn log 127 | * @method Log-warn 128 | */ 129 | WarnLog: function(test) { 130 | 131 | // Listen for warn logs 132 | Log.once('*.log-test.*', function(log, module, name) { 133 | test.equals(log, 'warn', 'The log type is correct'); 134 | test.equals(module, 'log-test', 'found the correct module name'); 135 | test.equals(name, 'warnLog', 'found the correct log name'); 136 | test.done(); 137 | }); 138 | log.warn('warnLog'); 139 | }, 140 | 141 | /** 142 | * Tests for the error log 143 | * @method Log-error 144 | */ 145 | ErrorLog: function(test) { 146 | 147 | // Listen for error logs 148 | Log.once('error.log-test.errorLog', function(log, module, name) { 149 | test.equals(log, 'error', 'The log type is correct'); 150 | test.equals(module, 'log-test', 'found the correct module name'); 151 | test.equals(name, 'errorLog', 'found the correct log name'); 152 | test.done(); 153 | }); 154 | log.error('errorLog'); 155 | }, 156 | 157 | /** 158 | * Tests for the fatal log 159 | * @method Log-fatal 160 | */ 161 | FatalLog: function(test) { 162 | 163 | // Listen for fatal logs 164 | Log.once('{error,fatal}.*.fatal[Ll]og', function(log, module, name) { 165 | test.equals(log, 'fatal', 'The log type is correct'); 166 | test.equals(module, 'log-test', 'found the correct module name'); 167 | test.equals(name, 'fatalLog', 'found the correct log name'); 168 | test.done(); 169 | }); 170 | log.fatal('fatalLog'); 171 | }, 172 | 173 | /** 174 | * Make sure a stat is output for every log 175 | * @method Log-Stat 176 | */ 177 | Stat: function(test) { 178 | 179 | // Listen for the stat 180 | Monitor.Stat.once('*', function(module, name, value, type) { 181 | test.equals(module, 'Log', 'The stat module is correct'); 182 | test.equals(name, 'info.log-test.Checking for stat emission', 'found the correct stat name'); 183 | test.equals(value, 1, 'Correctly incremented the log stat by 1'); 184 | test.equals(type, 'c', 'Correct stat type - counter'); 185 | test.done(); 186 | }); 187 | log.info('Checking for stat emission'); 188 | }, 189 | 190 | /** 191 | * Tests the Log probe 192 | * @method Log-ProbeTest 193 | */ 194 | ProbeTest: function(test) { 195 | var logMonitor = new Monitor({probeClass:'Log', initParams:{interval:10, pattern:'*.log-test.*'}}); 196 | logMonitor.connect(function(error) { 197 | test.ok(!error, 'Log monitor error: ' + JSON.stringify(error)); 198 | logMonitor.on('change', function() { 199 | var bundle = logMonitor.get('bundle'); 200 | 201 | // Omit the initial state of the monitor 202 | if (bundle.length === 0) { 203 | return; 204 | } 205 | 206 | test.ok(bundle, 'The log bundle is available'); 207 | test.equals(bundle.length, 1, 'There is a single log in the bundle'); 208 | var logArgs = bundle[0]; 209 | test.equals(logArgs.length, 5, 'There are 5 log arguments'); 210 | test.ok(Date.now() - (new Date(logArgs[0]).getTime()) < 1000, 'The first arg is a timestamp'); 211 | test.equals(logArgs[1], 'info', 'There log type is correct'); 212 | test.equals(logArgs[2], 'log-test', 'Ther module is correct'); 213 | test.equals(logArgs[3], 'probeTest', 'There log name is correct'); 214 | test.equals(logArgs[4], 22, 'The value is correct'); 215 | logMonitor.off('change'); 216 | test.done(); 217 | }); 218 | log.info('probeTest', 22); 219 | }); 220 | }, 221 | 222 | /** 223 | * Tests the probe streams multiple items at once 224 | * @method Log-ProbeStream 225 | */ 226 | ProbeStream: function(test) { 227 | 228 | // This relies on the fact that the monitor was created in the prior 229 | // function, and it just emitted a single item. 230 | var logMonitor = new Monitor({probeClass:'Log', initParams:{interval:10, pattern:'*.log-test.*'}}); 231 | logMonitor.connect(function(error) { 232 | test.ok(!error, 'Log monitor error: ' + JSON.stringify(error)); 233 | logMonitor.on('change', function() { 234 | var bundle = logMonitor.get('bundle'); 235 | 236 | // Omit the current state of the probe (first change event) 237 | if (bundle.length < 4) { 238 | return; 239 | } 240 | 241 | test.ok(bundle, 'The log bundle is available'); 242 | test.equals(bundle.length, 4, 'There were the correct number of items in the stream'); 243 | var logArgs = bundle[2]; 244 | test.equals(logArgs.length, 7, 'There are 7 log arguments'); 245 | test.ok(Date.now() - (new Date(logArgs[0]).getTime()) < 1000, 'The first arg is a timestamp'); 246 | test.equals(logArgs[1], 'error', 'The log name is correct'); 247 | test.equals(logArgs[2], 'log-test', 'The module is correct'); 248 | test.equals(logArgs[3], 'probeTest3', 'The log name is correct'); 249 | test.equals(logArgs[4], 420, 'The value is correct'); 250 | test.equals(logArgs[5], 'hello', 'The next arg is correct'); 251 | test.equals(logArgs[6].a, 'world', 'Objects are correctly represented'); 252 | logMonitor.off('change'); 253 | test.done(); 254 | }); 255 | log.info('probeTest1'); 256 | log.warn('probeTest2'); 257 | log.error('probeTest3', 420, "hello", {a:"world"}); 258 | log.fatal('probeTest4', 2840); 259 | }); 260 | } 261 | 262 | }; 263 | 264 | }(this)); 265 | -------------------------------------------------------------------------------- /test/ProbeTest.js: -------------------------------------------------------------------------------- 1 | // ProbeTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | Probe = Monitor.Probe, PollingProbe = Monitor.PollingProbe, 14 | Backbone = Monitor.Backbone, _ = Monitor._; 15 | 16 | /** 17 | * Unit tests for the Probe class. 18 | * @class ProbeTest 19 | */ 20 | 21 | /** 22 | * Tests for Probe functionality 23 | * 24 | * @method ProbeTest 25 | */ 26 | // Test that important namespaces are available 27 | var probeTests = module.exports['ProbeTest'] = { 28 | 29 | /** 30 | * Tests that Probe classes are in place 31 | * @method ProbeTest-Classes 32 | */ 33 | Classes: function(test) { 34 | test.ok(Probe.prototype instanceof Backbone.Model, 'The Probe data model is in place'); 35 | test.ok(Probe.List.prototype instanceof Backbone.Collection, 'The Probe.List collection is in place'); 36 | test.ok(PollingProbe.prototype instanceof Probe, 'The PollingProbe base constructor is in place'); 37 | test.done(); 38 | }, 39 | 40 | /** 41 | * Tests Probe instantiation 42 | * @method ProbeTest-Instantiate 43 | */ 44 | Instantiate: function(test) { 45 | var process = probeTests.processMonitor = new Monitor({probeClass:'Process', initParams:{a:'b', pollInterval:100}}); 46 | process.connect(function(err) { 47 | test.ifError(err); 48 | test.notEqual(process.get('probeId'), null, "Probe ID isn't null"); 49 | test.done(); 50 | }); 51 | }, 52 | 53 | /** 54 | * Test the same ID on subsequent probe instantiation with similar init params 55 | * @method ProbeTest-SameProbe 56 | */ 57 | SameProbe: function(test) { 58 | var process = new Monitor({probeClass:'Process', initParams:{pollInterval:100, a:'b'}}); 59 | process.connect(function(err) { 60 | test.ifError(err); 61 | test.notEqual(process.get('probeId'), null, "Probe ID isn't null"); 62 | test.equal(probeTests.processMonitor.get('probeId'), process.get('probeId'), "Probes are the same with similar init params."); 63 | process.disconnect(); 64 | test.done(); 65 | }); 66 | }, 67 | 68 | /** 69 | * Test that different init params result in a different probe 70 | * @method ProbeTest-DifferentProbe 71 | */ 72 | DifferentProbe: function(test) { 73 | var process = new Monitor({probeClass:'Process', initParams:{pollInterval:1000, a:'b'}}); 74 | process.connect(function(err) { 75 | test.ifError(err); 76 | test.notEqual(process.get('probeId'), null, "Probe ID isn't null"); 77 | test.notEqual(probeTests.processMonitor.get('probeId'), process.get('probeId'), "Probes are different with different init params."); 78 | process.disconnect(); 79 | test.done(); 80 | }); 81 | }, 82 | 83 | /** 84 | * Tests remote control functionality 85 | * @method ProbeTest-RemoteControl 86 | */ 87 | RemoteControl: function(test) { 88 | var monitor = probeTests.processMonitor; 89 | monitor.control('ping', function(error, result) { 90 | test.ifError(error); 91 | test.equals(result, 'pong', 'Ping returned pong'); 92 | test.done(); 93 | }); 94 | }, 95 | 96 | /** 97 | * Test remote control failure (no control method) 98 | * @method ProbeTest-RemoteControlFail 99 | */ 100 | RemoteControlFail: function(test) { 101 | var monitor = probeTests.processMonitor; 102 | monitor.control('pingPong', function(error, result) { 103 | test.ok(error != null, 'Correctly errored on un-available control method'); 104 | test.done(); 105 | }); 106 | }, 107 | 108 | /** 109 | * Test the change event 110 | * @method ProbeTest-ChangeEvent 111 | */ 112 | ChangeEvent: function(test) { 113 | var monitor = probeTests.processMonitor; 114 | var onChange = function(){ 115 | monitor.off('change', onChange); 116 | var changes = monitor.changedAttributes(); 117 | test.ok(_.size(changes) > 0, 'Attribute changes came through'); 118 | test.done(); 119 | }; 120 | monitor.on('change', onChange); 121 | }, 122 | 123 | /** 124 | * Tests that Probe clean up works 125 | * @method ProbeTest-Cleanup 126 | */ 127 | Cleanup: function(test) { 128 | var monitor = probeTests.processMonitor; 129 | monitor.disconnect(function(error) { 130 | test.ifError(error); 131 | test.done(); 132 | }); 133 | } 134 | 135 | }; 136 | 137 | }(this)); 138 | -------------------------------------------------------------------------------- /test/RecipeProbeTest.js: -------------------------------------------------------------------------------- 1 | // RecipeProbeTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | var testRecipe = null; 11 | 12 | /** 13 | * Unit tests for the Recipe class. 14 | * @class RecipeProbeTest 15 | */ 16 | 17 | // Dependencies 18 | var Monitor = require('../lib/index'), 19 | RecipeProbe = Monitor.RecipeProbe, 20 | Backbone = Monitor.Backbone, 21 | recipeMonitor = null, 22 | dataModelMonitor = null, 23 | derivedRecipeMonitor = null, 24 | _ = Monitor._, 25 | NL = '\n'; 26 | 27 | /** 28 | * Tests for verifying modules are loaded and exposed properly 29 | * 30 | * @method ModuleLoad 31 | */ 32 | module.exports.ModuleLoad = { 33 | 34 | setUp: function(callback) {callback();}, 35 | tearDown: function(callback) {callback();}, 36 | 37 | /** 38 | * Tests that Recipe is exposed and of the correct type 39 | * @method ModuleLoad-Recipe 40 | */ 41 | Recipe: function(test) { 42 | test.ok(RecipeProbe.prototype instanceof Backbone.Model, 'The data model is in place'); 43 | test.ok(RecipeProbe.prototype instanceof Monitor.Probe, 'It is a probe'); 44 | test.done(); 45 | } 46 | 47 | }; 48 | 49 | /** 50 | * Tests for a hand coded recipe stored in config/test.js 51 | * @method HandCoded 52 | */ 53 | module.exports['HandCoded'] = { 54 | 55 | /** 56 | * Test Recipe initialization 57 | * @method HandCoded-Initialize 58 | */ 59 | Initialize: function(test) { 60 | // Give the RecipeTest probe a chance to load 61 | setTimeout(function(){ 62 | recipeMonitor = new Monitor({probeName:'RecipeTest'}); 63 | recipeMonitor.connect(function(error) { 64 | test.ok(!error, 'No error on recipe connect'); 65 | test.ok(recipeMonitor.get('started'), 'Recipe is started on init'); 66 | 67 | // Get a monitor to the underlying data model the recipe manipulates 68 | dataModelMonitor = new Monitor({probeName:'DataModelTest'}); 69 | dataModelMonitor.connect(function(error1) { 70 | test.ok(!error1, 'No error on data model connect'); 71 | test.equal(dataModelMonitor.get('attr1'), 'attrValue1', 'Connected to the correct data model'); 72 | test.done(); 73 | }); 74 | }); 75 | }, 0); 76 | }, 77 | 78 | /** 79 | * Test recipe stopping 80 | * @method HandCoded-Stop 81 | */ 82 | Stop: function(test) { 83 | recipeMonitor.control('stop', function(error) { 84 | test.ok(!error, 'No error on stop'); 85 | test.ok(!recipeMonitor.get('started'), 'Recipe is stopped on stop'); 86 | test.done(); 87 | }); 88 | }, 89 | 90 | /** 91 | * Test recipe starting 92 | * @method HandCoded-Start 93 | */ 94 | Start: function(test) { 95 | recipeMonitor.control('start', function(error) { 96 | test.ok(!error, 'No error on start'); 97 | test.ok(recipeMonitor.get('started'), 'Recipe is started on start'); 98 | test.done(); 99 | }); 100 | }, 101 | 102 | /** 103 | * Tests that the script is run on change 104 | * @method HandCoded-ScriptRun 105 | */ 106 | ScriptRun: function(test) { 107 | 108 | // The test recipe is fired on change, and the script simply copies 109 | // the value of attr1 into attr2. 110 | var newValue = 'scriptRunTest'; 111 | 112 | // Look for attr2 changing from the test 113 | var changeFn = function() { 114 | 115 | // This is triggered twice - once on attr1 change, once on attr2 change. 116 | // Only perform the test on new attr2 value 117 | var attr2 = dataModelMonitor.get('attr2'); 118 | if (attr2 === newValue) { 119 | dataModelMonitor.off('change', changeFn); 120 | test.equals(attr2, newValue, 'The script was run'); 121 | test.done(); 122 | } 123 | }; 124 | 125 | // Set attr1 to the new value. This should trigger the recipe. 126 | dataModelMonitor.on('change', changeFn); 127 | dataModelMonitor.set('attr1', newValue); 128 | } 129 | 130 | }; 131 | 132 | /** 133 | * Tests that recipe autoStart works as advertised 134 | * @method Derived 135 | */ 136 | module.exports['Derived'] = { 137 | 138 | /** 139 | * Construct a derived recipe 140 | * @method Derived-Construct 141 | */ 142 | Construct: function(test) { 143 | 144 | // Build the class 145 | var DerivedRecipe = RecipeProbe.extend({ 146 | probeClass: 'DerivedRecipe', 147 | initialize: function(){ 148 | var t = this; 149 | t.set({ 150 | autoStart: false, 151 | monitors: { 152 | dataModel: {probeName: 'DataModelTest'} 153 | } 154 | }); 155 | RecipeProbe.prototype.initialize.apply(t, arguments); 156 | }, 157 | run: function(context){ 158 | var t = this; 159 | // Set derivedAttr2 to the value of derivedAttr1 160 | context.dataModel.set('derivedAttr2', context.dataModel.get('derivedAttr1')); 161 | } 162 | }); 163 | 164 | // Get a monitor to the class 165 | derivedRecipeMonitor = new Monitor({probeClass:'DerivedRecipe'}); 166 | derivedRecipeMonitor.connect(function(error){ 167 | test.ok(!error, 'No error on derived recipe connect'); 168 | test.equal(dataModelMonitor.get('derivedAttr1'), 'derivedAttrValue1', 'Data model is set for testing'); 169 | test.equal(dataModelMonitor.get('derivedAttr2'), null, 'Derived test not run yet'); 170 | test.done(); 171 | }); 172 | }, 173 | 174 | /** 175 | * Tests that a non-autoStart recipe does not start automatically 176 | * @method Derived-IsNotRunning 177 | */ 178 | IsNotRunning: function(test) { 179 | setTimeout(function(){ 180 | test.equal(derivedRecipeMonitor.get('started'), false, 'Test has not started'); 181 | test.equal(dataModelMonitor.get('derivedAttr2'), null, 'Still has not run'); 182 | test.done(); 183 | }, 10); 184 | }, 185 | 186 | /** 187 | * Tests that the recipe can start 188 | * @method AutoRun-CanStart 189 | */ 190 | CanStart: function(test) { 191 | derivedRecipeMonitor.control('start', function(error) { 192 | test.ok(!error, 'No error on start'); 193 | test.ok(derivedRecipeMonitor.get('started'), 'Recipe is started on start'); 194 | test.equal(dataModelMonitor.get('derivedAttr2'), null, 'First run still not performed'); 195 | test.done(); 196 | }); 197 | } 198 | 199 | 200 | }; 201 | 202 | 203 | }(this)); 204 | 205 | -------------------------------------------------------------------------------- /test/RouterTest.js: -------------------------------------------------------------------------------- 1 | // RouterTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | Router = Monitor.Router, Backbone = Monitor.Backbone, 14 | server, serverPort, internal, external, gateway, 15 | probeConnection, gatewayConnection, eventConnection, 16 | _ = Monitor._; 17 | 18 | /** 19 | * Unit tests for the Router class. 20 | * @class RouterTest 21 | */ 22 | 23 | /** 24 | * Tests for baseline Router functionality 25 | * 26 | * @method Router 27 | */ 28 | module.exports['Router'] = { 29 | 30 | /** 31 | * Create a Server to test routing with 32 | * @method Router-SetUp 33 | */ 34 | SetUp: function(test) { 35 | process.env.NODE_APP_INSTANCE = 'test'; 36 | server = new Monitor.Server(); 37 | server.start(test.done); 38 | }, 39 | 40 | /** 41 | * Tests that Router classes are in place 42 | * @method Router-Classes 43 | */ 44 | Classes: function(test) { 45 | test.ok(Router.prototype instanceof Backbone.Model, 'The Router data model is in place'); 46 | test.done(); 47 | }, 48 | 49 | /** 50 | * Test that the router finds an internal probe 51 | * @method Router-ConnectInternal 52 | */ 53 | ConnectInternal: function(test) { 54 | internal = new Monitor({probeClass:'Process', initParams:{a:'b'}}); 55 | internal.connect(function(error) { 56 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 57 | var probeId = internal.get('probeId'); 58 | test.ok(probeId, "The router found the internal probe"); 59 | test.done(); 60 | }); 61 | }, 62 | 63 | /** 64 | * Test that the same probe is connected when requested with the same initParams 65 | * @method Router-InternalSameProbe 66 | */ 67 | InternalSameProbe: function(test) { 68 | var other = new Monitor({probeClass:'Process', initParams:{a:'b'}}); 69 | other.connect(function(error) { 70 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 71 | var internalId = internal.get('probeId'); 72 | var otherId = other.get('probeId'); 73 | test.equal(internalId, otherId, "Two monitors created with the same initParams are connected to the same probe id: " + internalId); 74 | other.disconnect(test.done); 75 | }); 76 | }, 77 | 78 | /** 79 | * Test that different probes are connected when requested with the different initParams 80 | * @method Router-InternalDifferentProbe 81 | */ 82 | InternalDifferentProbe: function(test) { 83 | var other = new Monitor({probeClass:'Process', initParams:{a:'c'}}); 84 | other.connect(function(error) { 85 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 86 | var internalId = internal.get('probeId'); 87 | var otherId = other.get('probeId'); 88 | test.notEqual(internalId, otherId, "Two monitors created with the same initParams are connected to different probes"); 89 | other.disconnect(test.done); 90 | }); 91 | }, 92 | 93 | /** 94 | * Test that the router finds an external probe 95 | * @method Router-ConnectExternal 96 | */ 97 | ConnectExternal: function(test) { 98 | external = new Monitor({probeClass:'Process', hostName:'localhost', initParams:{a:'b'}}); 99 | external.connect(function(error) { 100 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 101 | var probeId = external.get('probeId'); 102 | test.ok(probeId, "The router found the remote probe based on class and host"); 103 | test.done(); 104 | }); 105 | }, 106 | 107 | /** 108 | * Test the getConnection method returns the connection 109 | * @method Router-GetConnection 110 | */ 111 | GetConnection: function(test) { 112 | var connection = external.getConnection(); 113 | test.ok(connection instanceof Monitor.Connection, "The remote connection is found"); 114 | test.ok(connection.get('remoteHostName'), "The connection as a remote host name"); 115 | test.done(); 116 | }, 117 | 118 | /** 119 | * Test that the same external probe is connected when requested with the same initParams 120 | * @method Router-ExternalSameProbe 121 | */ 122 | ExternalSameProbe: function(test) { 123 | var other = new Monitor({probeClass:'Process', hostName:'localhost', initParams:{a:'b'}}); 124 | other.connect(function(error) { 125 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 126 | var externalId = external.get('probeId'); 127 | var otherId = other.get('probeId'); 128 | test.equal(externalId, otherId, "Two monitors created with the same initParams are connected to the same probe id: " + externalId); 129 | other.disconnect(test.done); 130 | }); 131 | }, 132 | 133 | /** 134 | * Test that different external probes are connected when requested with the different initParams 135 | * @method Router-ExternalDifferentProbe 136 | */ 137 | ExternalDifferentProbe: function(test) { 138 | var other = new Monitor({probeClass:'Process', hostName:'localhost', initParams:{a:'c'}}); 139 | other.connect(function(error) { 140 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 141 | var externalId = external.get('probeId'); 142 | var otherId = other.get('probeId'); 143 | test.notEqual(externalId, otherId, "Two monitors created with the same initParams are connected to different probes"); 144 | other.disconnect(test.done); 145 | }); 146 | }, 147 | 148 | /** 149 | * Test that the router can connect with a probe in an app by instance ID 150 | * @method Router-ByAppInstance 151 | */ 152 | ByAppInstance: function(test) { 153 | // Don't connect locally 154 | delete process.env.NODE_APP_INSTANCE; 155 | 156 | external = new Monitor({probeClass:'Process', appInstance: 'test', initParams:{a:'b'}}); 157 | external.connect(function(error) { 158 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 159 | test.equal('test', external.probe.connection.get('remoteAppInstance'), 'Verified the probe connected to the right instance'); 160 | var probeId = external.get('probeId'); 161 | test.ok(probeId, "The router found the remote probe based on instance id"); 162 | test.done(); 163 | }); 164 | }, 165 | 166 | /** 167 | * Test that the local probe forwards change events 168 | * @method Router-InternalChanges 169 | */ 170 | InternalChanges: function(test) { 171 | var onChange = function() { 172 | test.ok(true, 'The change event was fired'); 173 | var changes = internal.changedAttributes(); 174 | test.ok(changes, 'Discovered changed attributes: ', _.keys(changes)); 175 | internal.off('change', onChange); 176 | test.done(); 177 | }; 178 | internal.on('change', onChange); 179 | }, 180 | 181 | /** 182 | * Test that the remote probe forwards change events 183 | * @method Router-ExternalChanges 184 | */ 185 | ExternalChanges: function(test) { 186 | var onChange = function() { 187 | test.ok(true, 'The change event was fired'); 188 | var changes = external.changedAttributes(); 189 | test.ok(changes, 'Discovered changed attributes: ', _.keys(changes)); 190 | external.off('change', onChange); 191 | test.done(); 192 | }; 193 | external.on('change', onChange); 194 | }, 195 | 196 | /** 197 | * Test that the router can route control an external probe 198 | * @method Router-ControlExternal 199 | */ 200 | ControlExternal: function(test) { 201 | external.control('ping', function(error, response) { 202 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 203 | test.ok(response, 'The response was ' + JSON.stringify(response)); 204 | test.equal(response, 'pong', "Sent a message to, and received the expected response."); 205 | test.done(); 206 | }); 207 | }, 208 | 209 | /** 210 | * Test that a disconnect to an internal probe works 211 | * @method Router-DisconnectInternal 212 | */ 213 | DisconnectInternal: function(test) { 214 | internal.disconnect(function(error) { 215 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 216 | internal = null; 217 | test.done(); 218 | }); 219 | }, 220 | 221 | /** 222 | * Test that a disconnect to an external probe works 223 | * @method Router-DisconnectExternal 224 | */ 225 | DisconnectExternal: function(test) { 226 | external.disconnect(function(error) { 227 | test.ok(!error, 'The error was ' + JSON.stringify(error)); 228 | test.done(); 229 | }); 230 | }, 231 | 232 | /** 233 | * Test that the external connection:add event is fired on add. 234 | * @method Router-ConnectionAddEvent 235 | */ 236 | ConnectionAddEvent: function(test) { 237 | var router = Monitor.getRouter(); 238 | var onConnect = function(connection) { 239 | eventConnection = connection; 240 | test.ok(connection !== null, 'The connection was established'); 241 | var hostName = connection.get('hostName'); 242 | test.ok(hostName === '127.0.0.1', 'The connection was with the expected host'); 243 | router.off('connection:add', onConnect); 244 | test.done(); 245 | }; 246 | router.on('connection:add', onConnect); 247 | external = new Monitor({probeClass:'Process', hostName:'127.0.0.1', initParams:{a:'b'}}); 248 | external.connect(); 249 | }, 250 | 251 | /** 252 | * Test that the external connection:remove event is fired on connection remove. 253 | * @method Router-ConnectionRemoveEvent 254 | */ 255 | ConnectionRemoveEvent: function(test) { 256 | var router = Monitor.getRouter(); 257 | var onDisconnect = function(connection) { 258 | test.ok(connection !== null, 'The connection was removed'); 259 | var hostName = connection.get('hostName'); 260 | test.ok(hostName === '127.0.0.1', 'The disconnect was with the expected host'); 261 | router.off('connection:remove', onDisconnect); 262 | test.done(); 263 | }; 264 | router.on('connection:remove', onDisconnect); 265 | router.removeConnection(eventConnection); 266 | }, 267 | 268 | /** 269 | * Tear down the test Server and disconnect the test probes 270 | * @method Router-TearDown 271 | */ 272 | TearDown: function(test) { 273 | server.stop(test.done); 274 | } 275 | 276 | }; 277 | 278 | }(this)); 279 | -------------------------------------------------------------------------------- /test/ServerTest.js: -------------------------------------------------------------------------------- 1 | // ServerTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | Server = Monitor.Server, Backbone = Monitor.Backbone; 14 | 15 | /** 16 | * Unit tests for the Server class. 17 | * @class ServerTest 18 | */ 19 | 20 | /** 21 | * Test group for baseline Server functionality 22 | * 23 | * @method Server 24 | */ 25 | module.exports['Server'] = { 26 | 27 | setUp: function(callback) {callback();}, 28 | tearDown: function(callback) {callback();}, 29 | 30 | /** 31 | * Tests that Server classes are in place 32 | * @method Server-Classes 33 | */ 34 | Classes: function(test) { 35 | test.ok(Server.prototype instanceof Backbone.Model, 'The Server data model is in place'); 36 | test.ok(Server.List.prototype instanceof Backbone.Collection, 'The Server.List collection is in place'); 37 | test.done(); 38 | }, 39 | 40 | /** 41 | * Start and Stop a server 42 | * @method Server-StartStop 43 | */ 44 | StartStop: function(test) { 45 | var server = new Monitor.Server(); 46 | server.on('start', function() { 47 | test.ok(server.get('port') > 0, 'The server started accepting connections on a port'); 48 | server.stop(function(){ 49 | test.ok(true, 'The server has stopped'); 50 | test.done(); 51 | }); 52 | }); 53 | server.start(); 54 | }, 55 | 56 | /** 57 | * Verify multiple servers start on different ports 58 | * @method Server-MultipleStartStop 59 | */ 60 | MultipleStartStop: function(test) { 61 | var server1 = new Monitor.Server(), port1, port2; 62 | server1.on('start', function() { 63 | port1 = server1.get('port'); 64 | test.ok(port1 >= Monitor.Config.Monitor.serviceBasePort, 'The server started in the correct port range'); 65 | var server2 = new Monitor.Server(); 66 | server2.on('start', function() { 67 | port2 = server2.get('port'); 68 | test.notEqual(port1, port2, 'Two servers started on two different ports'); 69 | server1.stop(function(){ 70 | server2.stop(function(){ 71 | test.ok(true, 'Both servers have stopped'); 72 | test.done(); 73 | }); 74 | }); 75 | }); 76 | server2.start(); 77 | }); 78 | server1.start(); 79 | } 80 | 81 | }; 82 | 83 | }(this)); 84 | -------------------------------------------------------------------------------- /test/StatTest.js: -------------------------------------------------------------------------------- 1 | // StatTest.js (c) 2010-2014 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | Stat = Monitor.Stat, Backbone = Monitor.Backbone, 14 | stat = Monitor.getStatLogger('stat-test'); 15 | 16 | /** 17 | * Unit tests for the Stat class. 18 | * @class StatTest 19 | */ 20 | 21 | /** 22 | * Test group for baseline Stat functionality 23 | * 24 | * @method Stat 25 | */ 26 | module.exports['Stat'] = { 27 | 28 | setUp: function(callback) {callback();}, 29 | tearDown: function(callback) {callback();}, 30 | 31 | /** 32 | * Tests that Stat class is in place 33 | * @method Stat-Classes 34 | */ 35 | Classes: function(test) { 36 | test.ok(Stat.prototype, 'The Stat model is in place'); 37 | test.done(); 38 | }, 39 | 40 | /** 41 | * Internal tests for Graphite type regular expressions 42 | * @method Stat-GraphiteRegExp 43 | */ 44 | GraphiteRegExp: function(test) { 45 | var builder = Stat._buildRegex; 46 | var re; 47 | 48 | // Test the normal cases 49 | re = builder('hello.world'); 50 | test.ok(re.test('hello.world'), 'normal case works'); 51 | test.ok(!re.test('hello.wurld'), 'must be exact'); 52 | test.ok(!re.test('Hello.wurld'), 'case sensitive'); 53 | test.ok(!re.test('hello'), 'must have all words'); 54 | test.ok(!re.test('Hello.world.now'), 'limited to just the specified fields'); 55 | 56 | // Test the * wildcard 57 | re = builder('hello.*.world'); 58 | test.ok(re.test('hello.nice.world'), 'normal case works'); 59 | test.ok(re.test('hello.cruel.world'), 'normal case works'); 60 | test.ok(!re.test('hello.cruel'), 'must have all segments'); 61 | test.ok(!re.test('hello'), 'must have all segments'); 62 | test.ok(!re.test('hello.nice.world.now'), 'too many segments'); 63 | re = builder('*.*.world'); 64 | test.ok(re.test('hello.nice.world'), 'normal case works'); 65 | test.ok(re.test('Gello.nice.world'), 'this works too'); 66 | test.ok(!re.test('Gello.nice'), 'but not this'); 67 | re = builder('*.*.*'); 68 | test.ok(re.test('a.b.c'), 'normal case works'); 69 | test.ok(re.test('1.2.3'), '123 works'); 70 | test.ok(!re.test('nice.world'), 'must have all fields'); 71 | 72 | // Test the [] character list or range 73 | re = builder('hello.[123].world'); 74 | test.ok(re.test('hello.1.world'), '1 case works'); 75 | test.ok(re.test('hello.3.world'), '3 case works'); 76 | test.ok(!re.test('hello.4.world'), '4 doesnt work'); 77 | re = builder('hello.[1-3].world'); 78 | test.ok(re.test('hello.1.world'), '1- case works'); 79 | test.ok(re.test('hello.3.world'), '3- case works'); 80 | test.ok(!re.test('hello.4.world'), '4- doesnt work'); 81 | 82 | // Test the {list,in,str} list functionality 83 | re = builder('hello.{world,dolly,kitty}'); 84 | test.ok(re.test('hello.world'), 'world works'); 85 | test.ok(re.test('hello.kitty'), 'kitty works'); 86 | test.ok(re.test('hello.dolly'), 'dolly works'); 87 | test.ok(!re.test('hello.there'), 'there doesnt work'); 88 | 89 | // Test the /regex/ functionality 90 | re = builder('/hello.world/'); 91 | test.ok(re.test('hello.world'), 'hello world works'); 92 | test.ok(!re.test('hello.kitty'), 'hello kitty fails'); 93 | re = builder('/hello.world/i'); 94 | test.ok(re.test('Hello World'), 'case insensitivity training'); 95 | 96 | test.done(); 97 | 98 | }, 99 | 100 | /** 101 | * Tests that registering for '*' emits all stats 102 | * @method Stat-AllStats 103 | */ 104 | AllStats: function(test) { 105 | 106 | // Listen for all stats 107 | Stat.once('*', function(module, name, value, type) { 108 | test.equals(module, 'stat-test', 'found the stat-test module when listening to *'); 109 | test.equals(name, 'Some.Stat', 'found Some.Stat when listening to *'); 110 | test.equals(value, 34, 'some.stat value is correct'); 111 | test.equals(type, 'c', 'some.stat type is correct'); 112 | test.done(); 113 | }); 114 | stat.increment('Some.Stat', 34); 115 | }, 116 | 117 | /** 118 | * Tests the gauge functionality 119 | * @method Stat-Gauge 120 | */ 121 | Gauge: function(test) { 122 | Stat.once('stat-test.gauge.stat', function(module, name, value, type) { 123 | test.equals(name, 'gauge.stat', 'gauge.stat emitted correctly'); 124 | test.equals(value, -6273993, 'gauge.stat value is correct'); 125 | test.equals(type, 'g', 'gauge.stat type is correct'); 126 | test.done(); 127 | }); 128 | stat.gauge('gauge.stat', -6273993); 129 | }, 130 | 131 | /** 132 | * Tests the time stat 133 | * @method Stat-Time 134 | */ 135 | Time: function(test) { 136 | Stat.once('stat-test.time.stat', function(module, name, value, type) { 137 | test.equals(name, 'time.stat', 'time.stat emitted correctly'); 138 | test.equals(value, 193, 'time.stat value is correct'); 139 | test.equals(type, 'ms', 'time.stat type is correct'); 140 | test.done(); 141 | }); 142 | stat.time('time.stat', 193); 143 | }, 144 | 145 | /** 146 | * Tests the increment stat 147 | * @method Stat-Increment 148 | */ 149 | Increment: function(test) { 150 | Stat.once('stat-test.incr1', function(module, name, value, type) { 151 | test.equals(name, 'incr1', 'incr1 stat emitted'); 152 | test.equals(value, 1, 'default increment of 1 found'); 153 | test.equals(type, 'c', 'incr1 type is correct'); 154 | }); 155 | Stat.once('stat-test.incr2', function(module, name, value, type) { 156 | test.equals(name, 'incr2', 'incr2 stat emitted'); 157 | test.equals(value, 44, 'incr2 value is correct'); 158 | test.equals(type, 'c', 'incr2 type is correct'); 159 | test.done(); 160 | }); 161 | stat.increment('incr1'); 162 | stat.increment('incr2', 44); 163 | }, 164 | 165 | /** 166 | * Tests the decrement stat 167 | * @method Stat-Decrement 168 | */ 169 | Decrement: function(test) { 170 | Stat.once('stat-test.decr1', function(module, name, value, type) { 171 | test.equals(name, 'decr1', 'decr1 stat emitted'); 172 | test.equals(value, -1, 'default decrement of 1 found'); 173 | test.equals(type, 'c', 'decr1 type is correct'); 174 | }); 175 | Stat.once('stat-test.decr2', function(module, name, value, type) { 176 | test.equals(name, 'decr2', 'decr2 stat emitted'); 177 | test.equals(value, -29344, 'decr2 value is correct'); 178 | test.equals(type, 'c', 'decr2 type is correct'); 179 | test.done(); 180 | }); 181 | stat.decrement('decr1'); 182 | stat.decrement('decr2', 29344); 183 | }, 184 | 185 | /** 186 | * Tests the stat probe 187 | * @method Stat-ProbeTest 188 | */ 189 | ProbeTest: function(test) { 190 | var statMonitor = new Monitor({probeClass:'Stat', initParams:{interval:10, pattern:'stat-test.*'}}); 191 | statMonitor.connect(function(error) { 192 | test.ok(!error, 'Stat monitor error: ' + JSON.stringify(error)); 193 | statMonitor.on('change', function() { 194 | var bundle = statMonitor.get('bundle'); 195 | 196 | // Omit the initial state of the monitor 197 | if (bundle.length === 0) { 198 | return; 199 | } 200 | 201 | test.ok(bundle, 'The stat bundle is available'); 202 | test.equals(bundle.length, 1, 'There is a single stat in the bundle'); 203 | var statArgs = bundle[0]; 204 | test.equals(statArgs.length, 5, 'There are 5 stat arguments'); 205 | test.ok(Date.now() - (new Date(statArgs[0]).getTime()) < 1000, 'The first arg is a timestamp'); 206 | test.equals(statArgs[1], 'stat-test', 'The module is correct'); 207 | test.equals(statArgs[2], 'probeTest', 'The stat name is correct'); 208 | test.equals(statArgs[3], 1, 'The value is correct'); 209 | test.equals(statArgs[4], 'c', 'There stat type is correct'); 210 | statMonitor.off('change'); 211 | test.done(); 212 | }); 213 | stat.increment('probeTest'); 214 | }); 215 | }, 216 | 217 | /** 218 | * Tests the probe streams multiple items at once 219 | * @method Stat-ProbeStream 220 | */ 221 | ProbeStream: function(test) { 222 | 223 | // This relies on the fact that the monitor was created in the prior 224 | // function, and it just emitted a single item. 225 | var statMonitor = new Monitor({probeClass:'Stat', initParams:{interval:10, pattern:'stat-test.*'}}); 226 | statMonitor.connect(function(error) { 227 | test.ok(!error, 'Stat monitor error: ' + JSON.stringify(error)); 228 | statMonitor.on('change', function() { 229 | var bundle = statMonitor.get('bundle'); 230 | 231 | // Omit the current state of the probe (first change event) 232 | if (bundle.length < 4) { 233 | return; 234 | } 235 | 236 | test.ok(bundle, 'The stat bundle is available'); 237 | test.equals(bundle.length, 4, 'There were the correct number of items in the stream'); 238 | var statArgs = bundle[2]; 239 | test.equals(statArgs.length, 5, 'There are 5 stat arguments'); 240 | test.ok(Date.now() - (new Date(statArgs[0]).getTime()) < 1000, 'The first arg is a timestamp'); 241 | test.equals(statArgs[1], 'stat-test', 'The module is correct'); 242 | test.equals(statArgs[2], 'statGauge', 'The stat name is correct'); 243 | test.equals(statArgs[3], 420, 'The value is correct'); 244 | test.equals(statArgs[4], 'g', 'There stat type is correct'); 245 | statMonitor.off('change'); 246 | test.done(); 247 | }); 248 | stat.increment('probeTestr'); 249 | stat.decrement('probeTest'); 250 | stat.gauge('statGauge', 420); 251 | stat.time('statTimer', 2840); 252 | }); 253 | } 254 | 255 | }; 256 | 257 | }(this)); 258 | -------------------------------------------------------------------------------- /test/SyncProbeTest.js: -------------------------------------------------------------------------------- 1 | // SyncProbeTest.js (c) 2012 Loren West and other contributors 2 | // May be freely distributed under the MIT license. 3 | // For further details and documentation: 4 | // http://lorenwest.github.com/node-monitor 5 | (function(root){ 6 | 7 | // This should be run before other tests to set up configurations 8 | process.env.NODE_ENV='test'; 9 | var config = require('config'); 10 | 11 | // Dependencies 12 | var Monitor = require('../lib/index'), 13 | Backbone = Monitor.Backbone, 14 | _ = Monitor._, 15 | Path = require('path'), 16 | FS = require('fs'), 17 | Sync = Monitor.Sync, 18 | FileProbe = Monitor.FileProbe, 19 | SyncProbe = Monitor.SyncProbe, 20 | FileSyncProbe = SyncProbe.FileSyncProbe; 21 | 22 | // Constants 23 | var TEST_FILE_DIR = FileSyncProbe.getRootPath() || __dirname + '/syncProbeTest', 24 | TEST_OBJECT = { 25 | testNumber:1, 26 | testString:"two", 27 | testObject:{some:"sub_object"}, 28 | testArray:[1, "two", 3] 29 | }, 30 | TEST_ID = '228237-a', 31 | JSON_CONTENT = JSON.stringify(TEST_OBJECT, null, 2), 32 | TEST_MODEL = new Backbone.Model(TEST_OBJECT), 33 | LIVE_SYNC_CLASS = Backbone.Model.extend({ 34 | sync: new Sync('LiveSyncTest') 35 | }); 36 | 37 | // Old style watch takes *forever* to connect 38 | var WATCH_CONNECT_TIME = 1200; 39 | 40 | // Initialize the server-side sync probes 41 | var DEFAULT_PROBE_NAME = 'FileSyncProbe'; 42 | SyncProbe.Config.defaultProbe = DEFAULT_PROBE_NAME; 43 | FileSyncProbe.setRootPath(TEST_FILE_DIR); 44 | 45 | /** 46 | * Unit tests for the SyncProbe probe. 47 | * @class SyncProbeTest 48 | */ 49 | 50 | /** 51 | * Test group for baseline SyncProbe functionality 52 | * 53 | * @method SyncProbe 54 | */ 55 | module.exports['SyncProbe'] = { 56 | 57 | /** 58 | * Tests that classes are in correct 59 | * @method SyncProbe-Classes 60 | */ 61 | Classes: function(test) { 62 | test.ok(SyncProbe.prototype instanceof Backbone.Model, 'The data model is in place'); 63 | test.ok(SyncProbe.prototype instanceof Monitor.Probe, 'It is a probe'); 64 | test.done(); 65 | }, 66 | 67 | /** 68 | * The server decides what specific type of SyncProbe to instantiate 69 | * for classes of data models. When a SyncProbe is requested, the 70 | * SyncProbe instance becomes an instance of a specific type. This 71 | * tests that this coersion is made. 72 | * 73 | * @method SyncProbe-Coerce 74 | */ 75 | Coerce: function(test) { 76 | var probe = new SyncProbe({className:'Book'},{}); 77 | test.equal(probe.probeClass, DEFAULT_PROBE_NAME, 'The SyncProbe was successfully coerced into a ' + DEFAULT_PROBE_NAME + ' probe.'); 78 | test.equal(probe.get('className'), 'Book', 'The probe instance is correctly set.'); 79 | test.done(); 80 | } 81 | 82 | }; 83 | 84 | /** 85 | * Test group for base Sync (non LiveSync) probe usage 86 | * 87 | * @method BaseSync 88 | */ 89 | module.exports['BaseSync'] = { 90 | 91 | /** 92 | * One time setup for BaseSync tests 93 | * @method BaseSync-SetUp 94 | */ 95 | SetUp: function(test) { 96 | // Clear the directory 97 | FileProbe.rm_rf(TEST_FILE_DIR, function(error) { 98 | FileProbe.mkdir_r(TEST_FILE_DIR, function(error) { 99 | test.ok(!error, "Able to create the test directory"); 100 | test.done(); 101 | }); 102 | }); 103 | }, 104 | 105 | /** 106 | * Tests that a new object without an ID gets an ID assigned 107 | * @method BaseSync-CreateWitoutId 108 | */ 109 | CreateWithoutId: function(test) { 110 | test.ok(TEST_MODEL.isNew(), "The test model is a new object"); 111 | TEST_MODEL.sync = new Sync('Test'); 112 | TEST_MODEL.save({}, function(error, result){ 113 | test.ok(!error, 'CreateId save error: ' + JSON.stringify(error)); 114 | test.ok(TEST_MODEL.has('id'), "An ID was generated for the new object"); 115 | test.done(); 116 | }); 117 | }, 118 | 119 | /** 120 | * Tests that a new object with an ID gets saved 121 | * @method BaseSync-CreateWithId 122 | */ 123 | CreateWithId: function(test) { 124 | TEST_MODEL = new Backbone.Model(TEST_OBJECT); 125 | test.ok(TEST_MODEL.isNew(), "The test model is a new object"); 126 | TEST_MODEL.sync = new Sync('Test'); 127 | TEST_MODEL.save({id: 'some/path'}, function(error, result){ 128 | test.ok(!error, 'CreateWithId save error: ' + JSON.stringify(error)); 129 | test.done(); 130 | }); 131 | }, 132 | 133 | /** 134 | * Tests that a fetch by ID works 135 | * @method BaseSync-FetchById 136 | */ 137 | FetchById: function(test) { 138 | var testId = "339284"; 139 | TEST_MODEL = new Backbone.Model(TEST_OBJECT); 140 | test.ok(TEST_MODEL.isNew(), "The test model is a new object"); 141 | TEST_MODEL.sync = new Sync('Test'); 142 | TEST_MODEL.save({id: testId}, function(error, result){ 143 | test.ok(!error, 'FetchById save error: ' + JSON.stringify(error)); 144 | 145 | // Now fetch it 146 | var newModel = new Backbone.Model({id:testId}); 147 | newModel.sync = new Sync('Test'); 148 | newModel.fetch(function(error) { 149 | test.ok(!error, 'FetchById fetch error: ' + JSON.stringify(error)); 150 | test.deepEqual(newModel.toJSON(), TEST_MODEL.toJSON(), 'Successful fetch'); 151 | test.done(); 152 | }); 153 | }); 154 | }, 155 | 156 | /** 157 | * Tests the model.destroy functionality 158 | * @method BaseSync-DestroyById 159 | */ 160 | DestroyById: function(test) { 161 | var testId = "339284"; 162 | TEST_MODEL = new Backbone.Model(TEST_OBJECT); 163 | test.ok(TEST_MODEL.isNew(), "The test model is a new object"); 164 | TEST_MODEL.sync = new Sync('Test'); 165 | TEST_MODEL.save({id: testId}, function(error, result){ 166 | test.ok(!error, 'DestroyById save error: ' + JSON.stringify(error)); 167 | 168 | // Now fetch it 169 | var newModel = new Backbone.Model({id:testId}); 170 | newModel.sync = new Sync('Test'); 171 | newModel.fetch(function(error) { 172 | test.ok(!error, 'DestroyById fetch error: ' + JSON.stringify(error)); 173 | test.deepEqual(newModel.toJSON(), TEST_MODEL.toJSON(), 'Successful fetch'); 174 | 175 | // Now destroy it 176 | var anotherNewModel = new Backbone.Model({id:testId}); 177 | anotherNewModel.sync = new Sync('Test'); 178 | anotherNewModel.destroy(function(error) { 179 | test.ok(!error, 'DestroyById destroy error: ' + JSON.stringify(error)); 180 | 181 | // Make sure it's gone 182 | var fullPath = Path.join(TEST_FILE_DIR, 'Test', testId + '.json'); 183 | FS.stat(fullPath, function(error, stats) { 184 | test.ok(error && error.code === 'ENOENT', 'File is removed.'); 185 | test.done(); 186 | }); 187 | }); 188 | }); 189 | }); 190 | }, 191 | 192 | /** 193 | * One time teardown up for BaseSync tests 194 | * @method BaseSync-TearDown 195 | */ 196 | TearDown: function(test) { 197 | FileProbe.rm_rf(TEST_FILE_DIR, function(error) { 198 | test.done(); 199 | }); 200 | } 201 | 202 | }; 203 | 204 | /** 205 | * Test group for Live model synchronization 206 | * 207 | * @method LiveSync 208 | */ 209 | module.exports['LiveSync'] = { 210 | 211 | /** 212 | * One time setup for LiveSync tests 213 | * @method LiveSync-SetUp 214 | */ 215 | SetUp: function(test) { 216 | // Clear the directory 217 | FileProbe.rm_rf(TEST_FILE_DIR, function(error) { 218 | FileProbe.mkdir_r(TEST_FILE_DIR, function(error) { 219 | test.ok(!error, "Able to create the test directory"); 220 | TEST_MODEL = new LIVE_SYNC_CLASS(TEST_OBJECT); 221 | test.ok(TEST_MODEL.isNew(), "The test model is a new object"); 222 | TEST_MODEL.save({id: TEST_ID}, function(error, result){ 223 | test.ok(!error, 'LiveSync-Setup save error: ' + JSON.stringify(error)); 224 | test.done(); 225 | }); 226 | }); 227 | }); 228 | }, 229 | 230 | /** 231 | * Tests LiveSync connection 232 | * @method LiveSync-Connect 233 | */ 234 | Connect: function(test) { 235 | var model = new LIVE_SYNC_CLASS({id: TEST_ID}); 236 | model.fetch({liveSync:true}, function(error) { 237 | test.ok(!error, "No error on liveSync fetch"); 238 | test.deepEqual(model.toJSON(), TEST_MODEL.toJSON(), 'Successful fetch'); 239 | model.clear(); 240 | test.done(); 241 | }); 242 | }, 243 | 244 | /** 245 | * Tests LiveSync disconnect 246 | * @method LiveSync-Disconnect 247 | */ 248 | Disconnect: function(test) { 249 | var model = new LIVE_SYNC_CLASS({id: TEST_ID}); 250 | model.fetch({liveSync:true}, function(error) { 251 | test.ok(!error, "No error on liveSync fetch"); 252 | test.ok(model.syncMonitor, "Live sync monitor is attached"); 253 | model.clear(); 254 | test.ok(!model.syncMonitor, "Live sync monitor is detached"); 255 | test.done(); 256 | }); 257 | }, 258 | 259 | /** 260 | * Tests that a client change in the data model persists to the server 261 | * @method LiveSync-ClientChange 262 | */ 263 | ClientChange: function(test) { 264 | var model = new LIVE_SYNC_CLASS({id: TEST_ID}); 265 | model.fetch({liveSync:true}, function(error) { 266 | test.ok(!error, "No error on liveSync fetch"); 267 | test.deepEqual(model.toJSON(), TEST_MODEL.toJSON(), 'Successful fetch'); 268 | model.set('testNumber', 2); 269 | 270 | // Wait a bit, then see if the file was updated onto the filesystem 271 | setTimeout(function(){ 272 | var path = Path.join(TEST_FILE_DIR, 'LiveSyncTest', TEST_ID + '.json'); 273 | var obj = JSON.parse(FS.readFileSync(path)); 274 | test.ok(_.isEqual(obj, model.toJSON()), 'Live sync did auto-save'); 275 | model.clear(); 276 | test.done(); 277 | }, WATCH_CONNECT_TIME); 278 | }); 279 | }, 280 | 281 | /** 282 | * Tests that a server change propagates the data to the client with a change event 283 | * @method LiveSync-ServerChange 284 | */ 285 | ServerChange: function(test) { 286 | var model = new LIVE_SYNC_CLASS({id: TEST_ID}); 287 | model.fetch({liveSync:true}, function(error) { 288 | test.ok(!error, "No error on liveSync fetch"); 289 | test.ok(model.get('testNumber') === 2, 'File still in the state last left'); 290 | 291 | // Watch for changes to the model 292 | var testStr = 'Hey diddle diddle'; 293 | var onChange = function() { 294 | test.equal(model.get('testString'), testStr, 'Server change was sent to the client'); 295 | model.off('change', onChange); 296 | model.clear(); 297 | test.done(); 298 | }; 299 | model.on('change', onChange); 300 | 301 | // Manually alter the file 302 | var path = Path.join(TEST_FILE_DIR, 'LiveSyncTest', TEST_ID + '.json'); 303 | var obj = model.toJSON(); 304 | obj.testString = testStr; 305 | FS.writeFileSync(path, JSON.stringify(obj, null, 2)); 306 | }); 307 | }, 308 | 309 | /** 310 | * Tests that a backend deletion is detected 311 | * @method LiveSync-ServerDelete 312 | */ 313 | ServerDelete: function(test) { 314 | var model = new LIVE_SYNC_CLASS({id: TEST_ID}); 315 | model.fetch({liveSync:true}, function(error) { 316 | test.ok(!error, "No error on liveSync fetch"); 317 | test.ok(model.get('testNumber') === 2, 'File still in the state last left'); 318 | 319 | // Watch for changes to the model 320 | var onChange = function() { 321 | test.ok(!model.get('id'), 'Model has been deleted'); 322 | model.off('change', onChange); 323 | model.clear(); 324 | test.done(); 325 | }; 326 | model.on('change', onChange); 327 | 328 | // Manually delete the file 329 | var path = Path.join(TEST_FILE_DIR, 'LiveSyncTest', TEST_ID + '.json'); 330 | FS.unlinkSync(path); 331 | }); 332 | }, 333 | 334 | /** 335 | * One time teardown up for BaseSync tests 336 | * @method LiveSync-TearDown 337 | */ 338 | TearDown: function(test) { 339 | FileProbe.rm_rf(TEST_FILE_DIR, function(error) { 340 | test.done(); 341 | }); 342 | } 343 | 344 | }; 345 | 346 | }(this)); 347 | -------------------------------------------------------------------------------- /yuidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monitor", 3 | "description": "Node Monitor", 4 | "version": "0.0.0", 5 | "year": "2014", 6 | "url": "http://lorenwest.github.com/node-monitor", 7 | "logo": "", 8 | "themedir": "./config/doc", 9 | "options": { 10 | "external": { 11 | }, 12 | "linkNatives": "true", 13 | "attributesEmit": "true", 14 | "paths": [ 15 | "./lib" 16 | ], 17 | "outdir": "./doc" 18 | } 19 | } 20 | --------------------------------------------------------------------------------