├── README.md ├── kedge.js ├── start.sh └── static ├── css ├── reset.css └── style.css ├── images ├── favicon.ico ├── hoover.png └── loggly.png ├── index.html └── js └── smoothie.js /README.md: -------------------------------------------------------------------------------- 1 | There is no README yet. 2 | -------------------------------------------------------------------------------- /kedge.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var io = require('socket.io'); 3 | var loggly = require('loggly'); 4 | var path = require('path'); 5 | var paperboy = require('paperboy'); 6 | 7 | // connect up to loggly 8 | var config = { 9 | subdomain: "geekceo", 10 | auth: { 11 | username: "kordless", 12 | password: "password" 13 | } 14 | }; 15 | var geekceo = loggly.createClient(config); 16 | 17 | // data for all currently connected clients, their searches, and the current bucket value 18 | // {"12345": {"searches": {"404": 99, "inputname:web": 99} } } 19 | var clients = {}; 20 | var numclients = 0; 21 | 22 | // a list of searches we're currently running + results 23 | // { '404': [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 'error': [ 1, 0, 0, 1, 2, 0, 0, 4, 1, 0 ] } 24 | var stashes = {}; 25 | 26 | // triggered by setInterval directly below 27 | var fetch = function () { 28 | // build our search list from all the client searches 29 | for (var client in clients) { 30 | // each search stored in each client 31 | for (var search in clients[client].searches) { 32 | if (!(search in stashes)) { 33 | // create array for results if not made 34 | stashes[search] = []; 35 | } 36 | } 37 | } 38 | // start retrieving each search 39 | for (var stash in stashes) { 40 | if (stashes[stash].length < 5) { 41 | // only if the array for the search is running low, do we get new results 42 | geekceo.facet('date', stash) 43 | .context({ buckets: 30, from: "NOW-2MINUTES", until: "NOW-1MINUTES" }) 44 | .run(function (err, results) { 45 | //console.log(results); 46 | // we're asnyc in here, so don't use non-unique externals 47 | // use the query in the response for finding the stash search term 48 | var query = results.context.query; 49 | 50 | // quick list so we can sort by date 51 | var ud = []; 52 | for (var x in results.data) { 53 | ud.push(x); 54 | } 55 | for (var x in ud.sort()) { 56 | // push on to stashes array for query/term/search 57 | stashes[query].push(results.data[ud[x]]); 58 | } 59 | }); 60 | } 61 | } 62 | } 63 | 64 | // run fetch above to check and/or populate stashes 65 | // make this interval * # of buckets above ~= 60K 66 | setInterval(fetch, 2000); 67 | 68 | // triggered by setInterval below 69 | // shifts data from each stash and dunks it into the the client's search values 70 | var dunk = function() { 71 | for (var stash in stashes) { 72 | // shift off the next entry for this search 73 | try { 74 | var foo = stashes[stash].shift(); 75 | } catch(err) { 76 | var foo = 0; 77 | } 78 | for (var client in clients) { 79 | // if client has this search/stash, update it 80 | if (stash in clients[client].searches) { 81 | clients[client].searches[stash] = foo; 82 | } 83 | // put in the number of currently connected clients 84 | clients[client].searches['numclients'] = numclients+""; 85 | } 86 | } 87 | } 88 | setInterval(dunk, 2000); 89 | 90 | // serve static content 91 | var server = http.createServer(function(req, res){ 92 | paperboy.deliver(path.join(path.dirname(__filename), 'static'), req, res); 93 | }); 94 | server.listen(80); 95 | 96 | // Create a Socket.IO instance, passing it our server 97 | var socket = io.listen(server); 98 | 99 | // Add a connect listener 100 | socket.on('connection', function(csock){ 101 | // put all of this client's searches in the clients struct 102 | var interval = null; 103 | var client_id = csock.sessionId+''; 104 | numclients++; 105 | clients[client_id]={"searches": {}}; 106 | csock.on('message',function(search){ 107 | clients[client_id].searches[search] = 0; 108 | geekceo.log('a3e839e9-4827-49aa-9d28-e18e5ba5a818', 'kedge: connect client-'+client_id, function (err, result) { }); 109 | }); 110 | csock.on('disconnect',function(){ 111 | delete clients[client_id]; 112 | numclients--; 113 | clearInterval(interval); 114 | geekceo.log('a3e839e9-4827-49aa-9d28-e18e5ba5a818', 'kedge: disconnect client-'+client_id, function (err, result) { }); 115 | }); 116 | 117 | // push data to client every second 118 | var ping = function() { 119 | //console.log(clients[client_id]); 120 | csock.send(clients[client_id]); 121 | } 122 | var interval = setInterval(ping, 1000); 123 | 124 | }); 125 | 126 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while true 3 | do 4 | node kedge.js 5 | done 6 | -------------------------------------------------------------------------------- /static/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | a:link, a:visited { 2 | color: #1485CC; 3 | text-decoration: none; 4 | } 5 | body { background-color: #000000; color: #bbb; } 6 | h2 { color: #999; margin: 0px 0px 5px 0px; font-family: Helvetica, Arial, sans-serif; font-size: 24px; } 7 | p { color: #999; font-family: Helvetica, Arial, sans-serif; font-size: 14px; line-height: 16px; margin-bottom: 4px; padding-top: 8px;} 8 | div { margin: 0px 20px 20px 20px; border-bottom: 1px dotted #333; } 9 | canvas { margin-bottom: 10px; border: 2px solid #333;} 10 | #logo { margin-top: 20px; padding-bottom: 10px; border: 0px;} 11 | #blurb { margin-top: 0px; padding-bottom: 10px; width: 1000px;} 12 | #footer { margin-top: 0px; padding-bottom: 10px; border: 0px;} 13 | .twitter-share-button { padding: 20px 0px 20px 4px; } 14 | -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kordless/kedge/729823cdbf70d00978908a379f93a9d7eac5337e/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/hoover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kordless/kedge/729823cdbf70d00978908a379f93a9d7eac5337e/static/images/hoover.png -------------------------------------------------------------------------------- /static/images/loggly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kordless/kedge/729823cdbf70d00978908a379f93a9d7eac5337e/static/images/loggly.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Smoothie Charts + #nodejs + #socketio + @loggly = Realtime WIN: 4 | 5 | 6 | 7 | 8 | 9 | 10 | 54 | 55 | 56 | 57 |

This page uses Node.js, Socket.io, Smoothie Charts, and Loggly to generate near-realtime graphs. Data is fed into Loggly via HTTP or Syslog based inputs and Node.js is used to query and cache the last minute's facet data for a given search. Data is streamed to web clients in realtime using Socket.io. You can grab the code on Github.

58 |

You can easily edit the data being tracked by creating a new canvas element in the HTML and then giving it a unique id and a title which contains the search term for your Loggly account.

59 | Tweet 60 |
61 | 62 | 63 |
64 |

Number of Clients Connected to Kedge

65 | 66 |
67 | 68 | 69 |
70 |

Tweets Matching 'Reddit'

71 | 72 |
73 | 74 | 75 |
76 |

Loggly Website Hits

77 | 78 |
79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /static/js/smoothie.js: -------------------------------------------------------------------------------- 1 | // MIT License: 2 | // 3 | // Copyright (c) 2010, Joe Walnes 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | /** 24 | * Smoothie Charts - http://smoothiecharts.org/ 25 | * (c) 2010, Joe Walnes 26 | */ 27 | 28 | function TimeSeries() { 29 | this.data = []; 30 | /** 31 | * The maximum value ever seen in this time series. 32 | */ 33 | this.max = undefined; 34 | /** 35 | * The minimum value ever seen in this time series. 36 | */ 37 | this.min = .0001; 38 | } 39 | 40 | TimeSeries.prototype.append = function(timestamp, value) { 41 | this.data.push([timestamp, value]); 42 | this.maxValue = this.maxValue ? Math.max(this.maxValue, value) : value; 43 | this.minValue = this.minValue ? Math.min(this.minValue, value) : value; 44 | }; 45 | 46 | function SmoothieChart(options) { 47 | // Defaults 48 | options = options || {}; 49 | options.grid = options.grid || { fillStyle:'#000000', strokeStyle: '#777777', lineWidth: 1, millisPerLine: 1000, verticalSections: 2 }; 50 | options.millisPerPixel = options.millisPerPixel || 20; 51 | options.labels = options.labels || { fillStyle:'#ffffff' } 52 | this.options = options; 53 | this.seriesSet = []; 54 | } 55 | 56 | SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { 57 | this.seriesSet.push({timeSeries: timeSeries, options: options || {}}); 58 | }; 59 | 60 | SmoothieChart.prototype.streamTo = function(canvas, delay) { 61 | var self = this; 62 | setInterval(function() { 63 | self.render(canvas, new Date().getTime() - (delay || 0)); 64 | }, 10); 65 | }; 66 | 67 | SmoothieChart.prototype.render = function(canvas, time) { 68 | var canvasContext = canvas.getContext("2d"); 69 | var options = this.options; 70 | var dimensions = {top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight}; 71 | 72 | // Save the state of the canvas context, any transformations applied in this method 73 | // will get removed from the stack at the end of this method when .restore() is called. 74 | canvasContext.save(); 75 | 76 | // Round time down to pixel granularity, so motion appears smoother. 77 | time = time - time % options.millisPerPixel; 78 | 79 | // Move the origin. 80 | canvasContext.translate(dimensions.left, dimensions.top); 81 | 82 | // Create a clipped rectangle - anything we draw will be constrained to this rectangle. 83 | // This prevents the occasional pixels from curves near the edges overrunning and creating 84 | // screen cheese (that phrase should neeed no explanation). 85 | canvasContext.beginPath(); 86 | canvasContext.rect(0, 0, dimensions.width, dimensions.height); 87 | canvasContext.clip(); 88 | 89 | // Clear the working area. 90 | canvasContext.save(); 91 | canvasContext.fillStyle = options.grid.fillStyle; 92 | canvasContext.fillRect(0, 0, dimensions.width, dimensions.height); 93 | canvasContext.restore(); 94 | 95 | // Grid lines.... 96 | canvasContext.save(); 97 | canvasContext.lineWidth = options.grid.lineWidth || 1; 98 | canvasContext.strokeStyle = options.grid.strokeStyle || '#ffffff'; 99 | // Vertical (time) dividers. 100 | if (options.grid.millisPerLine > 0) { 101 | for (var t = time - (time % options.grid.millisPerLine); t >= time - (dimensions.width * options.millisPerPixel); t -= options.grid.millisPerLine) { 102 | canvasContext.beginPath(); 103 | var gx = Math.round(dimensions.width - ((time - t) / options.millisPerPixel)); 104 | canvasContext.moveTo(gx, 0); 105 | canvasContext.lineTo(gx, dimensions.height); 106 | canvasContext.stroke(); 107 | canvasContext.closePath(); 108 | } 109 | } 110 | 111 | // Horizontal (value) dividers. 112 | for (var v = 1; v < options.grid.verticalSections; v++) { 113 | var gy = Math.round(v * dimensions.height / options.grid.verticalSections); 114 | canvasContext.beginPath(); 115 | canvasContext.moveTo(0, gy); 116 | canvasContext.lineTo(dimensions.width, gy); 117 | canvasContext.stroke(); 118 | canvasContext.closePath(); 119 | } 120 | // Bounding rectangle. 121 | canvasContext.beginPath(); 122 | canvasContext.strokeRect(0, 0, dimensions.width, dimensions.height); 123 | canvasContext.closePath(); 124 | canvasContext.restore(); 125 | 126 | // Calculate the current scale of the chart, from all time series. 127 | var maxValue = undefined; 128 | var minValue = undefined; 129 | 130 | for (var d = 0; d < this.seriesSet.length; d++) { 131 | // TODO(ndunn): We could calculate / track these values as they stream in. 132 | var timeSeries = this.seriesSet[d].timeSeries; 133 | if (timeSeries.maxValue) { 134 | maxValue = maxValue ? Math.max(maxValue, timeSeries.maxValue) : timeSeries.maxValue; 135 | } 136 | 137 | if (timeSeries.minValue) { 138 | minValue = minValue ? Math.min(minValue, timeSeries.minValue) : timeSeries.minValue; 139 | } 140 | } 141 | 142 | if (!maxValue && !minValue) { 143 | return; 144 | } 145 | 146 | var valueRange = maxValue - minValue; 147 | 148 | // For each data set... 149 | for (var d = 0; d < this.seriesSet.length; d++) { 150 | canvasContext.save(); 151 | var timeSeries = this.seriesSet[d].timeSeries; 152 | var dataSet = timeSeries.data; 153 | var seriesOptions = this.seriesSet[d].options; 154 | 155 | // Delete old data that's moved off the left of the chart. 156 | // We must always keep the last expired data point as we need this to draw the 157 | // line that comes into the chart, but any points prior to that can be removed. 158 | while (dataSet.length >= 2 && dataSet[1][0] < time - (dimensions.width * options.millisPerPixel)) { 159 | dataSet.splice(0, 1); 160 | } 161 | 162 | // Set style for this dataSet. 163 | canvasContext.lineWidth = seriesOptions.lineWidth || 1; 164 | canvasContext.fillStyle = seriesOptions.fillStyle; 165 | canvasContext.strokeStyle = seriesOptions.strokeStyle || '#ffffff'; 166 | // Draw the line... 167 | canvasContext.beginPath(); 168 | // Retain lastX, lastY for calculating the control points of bezier curves. 169 | var firstX = 0, lastX = 0, lastY = 0; 170 | for (var i = 0; i < dataSet.length; i++) { 171 | // TODO: Deal with dataSet.length < 2. 172 | var x = Math.round(dimensions.width - ((time - dataSet[i][0]) / options.millisPerPixel)); 173 | var value = dataSet[i][1]; 174 | var offset = maxValue - value; 175 | var scaledValue = Math.round((offset / valueRange) * dimensions.height); 176 | var y = Math.max(Math.min(scaledValue, dimensions.height - 1), 1); // Ensure line is always on chart. 177 | 178 | if (i == 0) { 179 | firstX = x; 180 | } 181 | // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/B�zier_curve#Quadratic_curves 182 | // 183 | // Assuming A was the last point in the line plotted and B is the new point, 184 | // we draw a curve with control points P and Q as below. 185 | // 186 | // A---P 187 | // | 188 | // | 189 | // | 190 | // Q---B 191 | // 192 | // Importantly, A and P are at the same y coordinate, as are B and Q. This is 193 | // so adjacent curves appear to flow as one. 194 | // 195 | canvasContext.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop 196 | Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) 197 | Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) 198 | x, y); // endPoint (B) 199 | 200 | lastX = x, lastY = y; 201 | } 202 | if (dataSet.length > 0 && seriesOptions.fillStyle) { 203 | // Close up the fill region. 204 | canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); 205 | canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); 206 | canvasContext.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); 207 | canvasContext.fill(); 208 | } 209 | canvasContext.stroke(); 210 | canvasContext.closePath(); 211 | canvasContext.restore(); 212 | } 213 | 214 | // Draw the axis values on the chart. 215 | if (!options.labels.disabled) { 216 | canvasContext.fillStyle = options.labels.fillStyle; 217 | var maxValueString = maxValue.toFixed(2); 218 | var minValueString = minValue.toFixed(2); 219 | canvasContext.fillText(maxValueString, dimensions.width - canvasContext.measureText(maxValueString).width - 2, 10); 220 | canvasContext.fillText(minValueString, dimensions.width - canvasContext.measureText(minValueString).width - 2, dimensions.height - 2); 221 | } 222 | 223 | canvasContext.restore(); // See .save() above. 224 | } 225 | --------------------------------------------------------------------------------