UNPKG

happner

Version:

distributed application engine with evented storage and mesh services

1,044 lines (746 loc) 23.6 kB
[&#9664;](https://github.com/happner/happner#documentation) contents ## The Basics This includes: * Creating mesh nodes with basic configuration. * Creating modules to run as components in the mesh. * Creating an endpoint connecting one mesh node to another. * Calling a method on a remote node. * Emitting events. * Subscribing to events with browser client. * Sundry other stuff to make charts in browser. **Here is the result of this walkthrough: [happner-demo#01-the-basics](https://github.com/happner/happner-demo/tree/01-the-basics).** ### Contents * [Create a demo project](#create-a-demo-project) * [Create the Master node module](#create-the-master-node-module) * [Create config for the Master node](#create-config-for-the-master-node) * [Create bin runner for the Master node](#create-bin-runner-for-the-master-node) ### * [Use env file for stage config](#use-env-file-for-stage-config) ### * [Create the Agent node module](#create-the-agent-node-module) * [Create the Agent mesh runner and config](#create-the-agent-mesh-runner-and-config) ### * [Create Start and Stop methods on Master and Agent components](#create-start-and-stop-methods-on-master-and-agent-components) * [Create report function on Master](#create-report-function-on-master) * [Call report function from Agent](#call-report-function-from-agent) * [Add configurable list of inspectors for Agent](#add-configurable-list-of-inspectors-for-agent) * [Update Master to emit event with each received metric](#update-master-to-emit-event-with-each-received-metric) ### * [Serve browser content from Master](#serve-browser-content-from-master) * [Create login script](#create-login-script) * [Create client script and style](#create-client-script-and-style) * [Install smoothie charts](#install-smoothie-charts) * [Load scripts into browser](#load-scripts-into-browser) *** ### Create a demo project [&#9650;](#) ```bash mkdir happner-demo cd happner-demo npm init # and fill with defaults npm install happner --save ``` ### Create the Master node module [&#9650;](#) This creates the mesh module that will run as the monitoring service's master node. **Note: Master is it's own node_module! This simplifies the configurations.** ```bash mkdir node_modules/master cd node_modules/master/ npm init # keep index.js as entry point vi index.js # see content below cd ../../ # cd - ``` Content of `./node_modules/master/index.js` ```javascript module.exports = Master; /* * Master class (runs as mesh component) * * @api public * @constructor * */ function Master() { console.log('new master'); } ``` ### Create config for the Master node [&#9650;](#) The config is `module.export`ed from a javascript file. ```bash mkdir config vi config/master.js ``` Content of `./config/master.js` ```javascript module.exports = { // This name will be used when attaching other nodes to this one. name: 'MasterNode', // Datalayer and network layer are the same thing. datalayer: { // host: '0.0.0.0', port: 50505, // Listening port persist: false, // Persist data across restarts? (later) secure: false, // Secure? (later) }, // // modules only necessary upon deviation from default // // https://github.com/happner/happner/blob/master/docs/configuration.md#module-config // modules: { // 'master': { // path: 'to/alternative/location' // } // }, // Include master as component // It assumes that 'master' is an installed node_module which exports 1 class components: { 'master': { } } } ``` ### Create bin runner for the Master node [&#9650;](#) This is the "executable" that runs the Master node. ```bash mkdir bin touch bin/master chmod +x bin/master vi bin/master ``` Content of `./bin/master` ```javascript #!/usr/bin/env node var Happner = require('happner'); var Config = require('../config/master'); // Call create() factory which returns the promise of a mesh or error Happner.create(Config) .then(function(mesh) { // got running mesh }) .catch(function(error) { console.error(error.stack || error.toString()) process.exit(1); }); ``` **At this point it should be possible to start the `bin/master` process and `^c` to stop it** *** ### Use env file for stage config [&#9650;](#) Install env file loader and create env file ``` npm install dotenv --save vi .env ``` Content of `./.env` ```env # change to ip accessable from remote MASTER_IP=0.0.0.0 MASTER_PORT=50505 ``` Update `./config/master.js` ```javascript // insert at start of file require('dotenv').load(); // and modify datalayer in config ... datalayer: { host: process.env.MASTER_IP, port: process.env.MASTER_PORT, persist: false, // Persist data across restarts? (later) secure: false, // Secure? (later) }, ... ``` **Important: Both `bin/master` and `bin/agent` expect to find `.env` file in the current diretctory, so don't cd into `bin/` to run them.** *** ### Create the Agent node module [&#9650;](#) This agent is installed into a mesh node running at each host to be monitored. It connects an `endpoint` to the master to report metrics. **Note: Agent is it's own node_module! This simplifies the configurations.** ```bash mkdir node_modules/agent cd node_modules/agent/ npm init # keep index.js as entry point vi index.js # see content below cd - # cd ../../ ``` Content of `./node_modules/agent/index.js` ```javascript module.exports = Agent; /* * Agent class (runs as mesh component) * * @api public * @constructor * */ function Agent() { console.log('new agent'); } ``` ### Create the Agent mesh runner and config [&#9650;](#) Same as Master, create config and bin files for Agent. **Note: Agent config includes an endpoint connecting to the Master** Content of `./config/agent.js` ```javascript require('dotenv').load(); module.exports = { // Allow default name // name: 'agent', datalayer: { port: 0, // Listen at random port (allows more than one agent instance per host) persist: false, // No storage secure: false, // Secure? (later) }, // Connect endpoint to MasterNode endpoints: { 'MasterNode': { config: { host: process.env.MASTER_IP, port: process.env.MASTER_PORT, // // Secure? (later) // username: '', // password: '', } } }, // Include agent as component components: { 'agent': { } } } ``` Content of `./bin/agent` ```javascript #!/usr/bin/env node var Happner = require('happner'); var Config = require('../config/agent'); // Call create() factory which returns the promise of a mesh or error Happner.create(Config) .then(function(mesh) { // got running mesh }) .catch(function(error) { console.error(error.stack || error.toString()) process.exit(1); }); ``` Remember to make agent executable: ```bash chmod +x bin/agent ``` **At this point is should be possible to start both `bin/master` and `bin/agent`.** *** ### Create Start and Stop methods on Master and Agent components [&#9650;](#) Start and Stop methods are used to assemble and tear down the component runtime. Additionally the `$happn` service can optionally be injected to perform any necessary interactions with the mesh. Update `./node_modules/master/index.js` ```javascript // Add these functions after constructor /* * Start method (called at mesh start(), if configured) * * @api public * @param {ComponentInstance} $happn - injected by the mesh when it calls this function * @param {Function} callback * */ Master.prototype.start = function($happn, callback) { //Agent.proto... $happn.log.info('starting master component'); callback(); } /* * Stop method (called at mesh stop(), if configured) * * @api public * @param {ComponentInstance} $happn - injected by the mesh when it calls this function * @param {Function} callback * */ Master.prototype.stop = function($happn, callback) { //Agent.proto... $happn.log.info('stopping master component'); callback(); } ``` Update `./config/master.js` ```javascript // Modify component declaration to include start and stop methods ... components: { // 'agent': { 'master': { startMethod: 'start', stopMethod: 'stop', } } ... ``` **ALSO** Do the same for `./node_modules/agent/index.js` and `./config/agent.js` ### Create report function on Master [&#9650;](#) This is a function defined on the master that will be repetatively called by the agents to report their metrics. **Note: A more elegant design might be for the agent to emit metrics and the master to be subscribed. But this would require an endpoint connection from master pointing to every agent. ie. The existing endpoint from agent to master is not bi-directional** Update `./node_modules/master/index.js` ```javascript // Add after start and stop functions /** * Metric object * * @typedef Metric * @type {object} * @property {Number} ts - utc timestamp * @property {String} key * @property {Number} val * */ /* * Report metric method (called by remote agents across the exchange) * * @api public * @param {ComponentInstance} $happn - injected by the mesh when it calls this function * @param {String} hostname - of the agent * @param {Metric} metric * @param {Function} callback * */ Master.prototype.reportMetric = function($happn, hostname, metric, callback) { $happn.log.info("metric from '%s': %j", hostname, metric); callback(null, {thank: 'u'}); } ``` ### Call report function from Agent [&#9650;](#) Functions on the master (being an endpoint) become available on the Agent via the exchange. Reminder: `./configs/agent.js` specifies **startMethod** and **stopMethod** in `components/agent/`. Using the Agent's start method, set up an interval that calls `reportMetric()` on the Master Update `./node_modules/agent/index.js` ```javascript // up top var os = require('os'); // update start and stop methods: /* * Start method (called at mesh start(), if configured) * * @api public * @param {ComponentInstance} $happn - injected by the mesh when it calls this function * @param {Function} callback * */ Agent.prototype.start = function($happn, callback) { $happn.log.info('starting agent component'); var hostname = os.hostname(); this.interval = setInterval(function() { var metric = { ts: Date.now(), key: 'test/metric', val: 1, } // call remote function exchange.<endpoint>.<component>.<method> $happn.exchange.MasterNode.master.reportMetric(hostname, metric, function(err, res) { // callback as called by master.reportMetric if (err) return $happn.log.error('from reportMetric', err); $happn.log.info('result from reportMetric: %j', res); }); }, 1000); callback(); } /* * Stop method (called at mesh stop(), if configured) * * @api public * @param {ComponentInstance} $happn - injected by the mesh when it calls this function * @param {Function} callback * */ Agent.prototype.stop = function($happn, callback) { $happn.log.info('stopping agent component'); // stop the interval running when component is stopped clearInterval(this.interval); callback(); } ``` **Note: The stop method explicitly undoes what the start method did (clearInterval) - this allows for components to be dynamically added and removed from the mesh without leaving things behind.** ### Add configurable list of inspectors for Agent [&#9650;](#) Because the config is a javascript file it is possible to pass functions as config. Add custom item onto component config for agent inspector functions (keyed on metric name) Update `./configs/agent.js` ```javascript // modify component config to include list of inspectors ... components: { 'agent': { startMethod: 'start', stopMethod: 'stop', inspectors: { // keeping these inspectors as selfcontained "lambdas" // means they could conceivably be configured on the // master, and dynamically propagated on change to // all agents (with eval on the agent (unfortunately?)) 'load/average-1': { interval: 1000, fn: function(callback) { var os = require('os'); callback(null, os.loadavg()[0]); } }, // 'load/average-5': { // interval: 1000, // fn: function(callback) { // var os = require('os'); // callback(null, os.loadavg()[1]); // } // }, // 'load/average-15': { // interval: 1000, // fn: function(callback) { // var os = require('os'); // callback(null, os.loadavg()[2]); // } // }, 'memory/percent-free': { interval: 1000, fn: function(callback) { var os = require('os'); var total = os.totalmem(); var free = os.freemem(); var percent = Math.round(free / total * 100 * 1000) / 1000; // to 3 decimal places callback(null, percent); } } } } } ... ``` And update the Agent module (`start()` and `stop()` functions) to use this new config. Update `./node_modules/agent/index.js` ```javascript /* * Start method (called at mesh start(), if configured) * * @api public * @param {ComponentInstance} $happn - injected by the mesh when it calls this function * @param {Function} callback * */ Agent.prototype.start = function($happn, callback) { $happn.log.info('starting agent component'); var hostname = os.hostname(); var inspectors = $happn.config.inspectors; Object.keys(inspectors).forEach(function(key) { var interval = inspectors[key].interval || 10000; var inspect = inspectors[key].fn; // run multiple inspectors each in separate interval inspectors[key].runner = setInterval(function() { // TODO: properly deal with inspector taking longer than interval inspect(function(error, result) { if (error) return $happn.log.error("inspector at key: '%s' failed", key, error); // submit inspect result to master var metric = { ts: Date.now(), key: key, val: result } $happn.exchange.MasterNode.master.reportMetric(hostname, metric, function(error, result) { // callback as called by master.reportMetric if (error) return $happn.log.error('from reportMetric', error); // $happn.log.info('result from reportMetric: %j', result); }); }); }, interval); }); callback(); } /* * Stop method (called at mesh stop(), if configured) * * @api public * @param {ComponentInstance} $happn - injected by the mesh when it calls this function * @param {Function} callback * */ Agent.prototype.stop = function($happn, callback) { $happn.log.info('stopping agent component'); // stop all inspector intervals var inspectors = $happn.config.inspectors; Object.keys(inspectors).forEach(function(key) { clearInterval(inspectors[key].runner); }); callback(); } ``` ### Update Master to emit event with each received metric [&#9650;](#) A browser in the client will be subscribing to these events Update reportMetric() in `./node_modules/master/index.js` ```javascript /* * Report metric method (called by remote agents across the exchange) * * @api public * @param {ComponentInstance} $happn - injected by the mesh when it calls this function * @param {String} hostname - of the agent * @param {Metric} metric * @param {Function} callback * */ Master.prototype.reportMetric = function($happn, hostname, metric, callback) { var eventKey = 'metrics/' + hostname + '/' + metric.key; var eventData = metric; $happn.log.debug("emitting '%s': '%j'", eventKey, eventData); $happn.emit(eventKey, eventData); callback(); } ``` **Note: The debug log message will not be seen unless util.logLevel is set to 'debug' or the process is started LOG_LEVEL environment variable** eg. ```bash LOG_LEVEL=debug bin/master LOG_COMPONENTS=master,another LOG_LEVEL=debug bin/master ``` *** ### Serve browser content from Master [&#9650;](#) Create a directory for static content containing index.html ```bash mkdir node_modules/master/app touch node_modules/master/app/index.html ``` Add web route to static content in Master component config. Update `./configs/master.js` ```javascript .. components: { 'master': { startMethod: 'start', stopMethod: 'stop', web: { routes: { // serves static content in node_modules/master/app at http://.../master/app 'app': 'static' } } } } .. ``` ### Create login script [&#9650;](#) This script is used to connect to the mesh. Content of `./node_modules/master/app/login.js` ```javascript (function(context) { // defaults to page address var options = { // host: '', // port: 80 } // unnecessary: secure not set true in mesh/datalayer config var credentials = { // username: '', // password: '', } var client = new MeshClient(options); client.login(credentials); // .then(... client.on('login/deny', function(error) { console.error(error); alert(error.toString()) }); client.on('login/error', function(error) { console.error(error); alert(error.toString()) }); // run client on login success client.on('login/allow', function() { context.runClient(client); }); })(this); ``` ### Create client script and style [&#9650;](#) This script is called after successfull login with the connected client. It subscribes to `metrics/*` and accordingly builds graphs into the browser. Content of `./node_modules/master/app/client.js` ```javascript (function(context) { context.runClient = function(client) { var hosts = {}; client.event.master.on('metrics/*', function(data, meta) { // extract hostname/chart/item from event path var pathPart = meta.path.match(/metrics\/(.*)$/)[1]; var keys = pathPart.split('/'); var hostname = keys.shift(); var chartname = keys.shift(); var itemname = keys.join('/'); var metric = data; updateMetric(hostname, chartname, itemname, metric); }); var updateMetric = function(hostname, chartname, itemname, metric) { ensureHost(hostname); ensureHostChart(hostname, chartname); ensureHostChartItem(hostname, chartname, itemname); updateHostChartItem(hostname, chartname, itemname, metric); } // ensure host element in document var ensureHost = function(hostname) { if (typeof hosts[hostname] !== 'undefined') return; var host = document.createElement("div"); host.id = 'host-' + hostname; host.className = 'host'; var heading = document.createElement("div"); heading.className = 'host-heading'; heading.innerHTML = hostname; host.appendChild(heading); var content = document.createElement("div"); content.className = 'host-content'; host.appendChild(content); document.body.appendChild(host); hosts[hostname] = { root: host, content: content, charts: {}, lastWrite: Date.now() } } // ensure chart element in host element in document var ensureHostChart = function(hostname, chartname) { if (typeof hosts[hostname].charts[chartname] !== 'undefined') return; var container = document.createElement("div"); container.className = 'chart'; var heading = document.createElement("div"); heading.className = 'chart-heading'; heading.innerHTML = chartname; container.appendChild(heading); var canvas = document.createElement("canvas"); canvas.id = 'canvas-' + hostname + '-' + chartname; canvas.className = 'chart-canvas'; canvas.width = 500; canvas.height = 100; container.appendChild(canvas); var host = hosts[hostname]; host.content.appendChild(container); var options = { maxValueScale: 1.02, minValueScale: 1.02, labels: { fillStyle: '#aaaaaa' } }; var chart = new SmoothieChart(options); chart.streamTo(canvas); host.charts[chartname] = { // canvas: canvas, heading: heading, chart: chart, items: {} } } // ensure item (line) in host/chart var ensureHostChartItem = function(hostname, chartname, itemname) { if (typeof hosts[hostname].charts[chartname].items[itemname] !== 'undefined') return; var host = hosts[hostname]; var chart = host.charts[chartname]; var series = new TimeSeries(); var options = {strokeStyle: 'rgba(0, 255, 0, 1)', fillStyle: 'rgba(0, 255, 0, 0.15)', lineWidth: 1}; chart.chart.addTimeSeries(series, options); chart.items[itemname] = { series: series } var heading = chartname + " (" + Object.keys(chart.items).join(', ') + ")"; chart.heading.innerHTML = heading; } // update item var updateHostChartItem = function(hostname, chartname, itemname, metric) { var host = hosts[hostname]; var chart = host.charts[chartname]; var item = chart.items[itemname]; item.series.append(metric.ts, metric.val); host.lastWrite = Date.now(); } // watch for hosts being removed setInterval(function() { var now = Date.now(); Object.keys(hosts).forEach(function(hostname) { var host = hosts[hostname]; if (now - host.lastWrite < 7000) return; document.body.removeChild(host.root); delete hosts[hostname]; }); }, 1000); } })(this); ``` Content of `./node_modules/master/app/client.css` ```css body { background-color: black; } .host { border: 1px solid grey; width: 520px; padding-bottom: 20px; margin-bottom: 4px; } .host-heading { color: rgba(255, 255, 255, 0.8); font-size: 1.2em; font-family: courier; text-align: center; } .chart-heading { padding-top: 7px; color: rgba(255, 255, 255, 0.4); width: 500px; font-family: courier; font-size: 1em; text-align: center; } .chart { position: relative; top: 50%; left: 50%; margin-left: -250px; } ``` ### Install smoothie charts [&#9650;](#) Using smoothie charts to graph streaming data. Install into master app directory ```bash cd node_modules/master/app wget http://github.com/joewalnes/smoothie/raw/master/smoothie.js cd - # cd ../../../ ``` ### Load scripts into browser [&#9650;](#) Content of `./node_modules/master/app/index.html` ```html <html> <head> <!-- get built-in api client script from mesh this defines MeshClient class --> <script type="text/javascript" src="/api/client"></script> <script type="text/javascript" src="/master/app/smoothie.js"></script> <!-- load app client this defines window.runClient() --> <script type="text/javascript" src="/master/app/client.js"></script> <link rel='stylesheet' href='/master/app/client.css'></link> <!-- connect to mesh this calls window.runClient() with the connected client instance --> <script type="text/javascript" src="/master/app/login.js"></script> </head> </html> ``` Start `bin/master` and `bin/agent`. And connect to [http://MASTER_IP:MASTER_PORT/master/app](http://127.0.0.1:50505/master/app)