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.
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 |
--------------------------------------------------------------------------------