Merge lp:~lucio.torre/graphite/add-events into lp:~graphite-dev/graphite/main
- add-events
- Merge into main
Status: | Merged |
---|---|
Approved by: | chrismd |
Approved revision: | 310 |
Merge reported by: | chrismd |
Merged at revision: | not available |
Proposed branch: | lp:~lucio.torre/graphite/add-events |
Merge into: | lp:~graphite-dev/graphite/main |
Diff against target: |
931 lines (+549/-38) 17 files modified
docs/install.rst (+2/-0) docs/who-is-using.rst (+1/-0) requirements.txt (+1/-0) setup.py (+4/-1) webapp/content/js/jquery.graphite.js (+152/-27) webapp/graphite/browser/urls.py (+1/-1) webapp/graphite/events/models.py (+49/-0) webapp/graphite/events/urls.py (+21/-0) webapp/graphite/events/views.py (+77/-0) webapp/graphite/graphlot/views.py (+4/-1) webapp/graphite/render/functions.py (+154/-2) webapp/graphite/settings.py (+2/-0) webapp/graphite/templates/browserHeader.html (+1/-0) webapp/graphite/templates/event.html (+30/-0) webapp/graphite/templates/events.html (+42/-0) webapp/graphite/templates/graphlot.html (+6/-5) webapp/graphite/urls.py (+2/-1) |
To merge this branch: | bzr merge lp:~lucio.torre/graphite/add-events |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
chrismd | Approve | ||
Sidnei da Silva (community) | Approve | ||
Review via email: mp+69142@code.launchpad.net |
Commit message
Description of the change
Add events to graphite.
Events are instantaneous occurrences that we want to track and correlate with our current metrics. Sample events are rollouts, reboots, errors, etc.
Events store the following information: summary, date, tags and "extra data", where you can store whatever you might need. This can all be edited from the admin: http://
You can add events from the command line using curl:
$ curl -X POST http://
So its very easy to integrate with other tools.
You can see the list of events from: http://
Then, using the graphlot ui you can overlay events into a graph by selecting tags that would filter the events or just '*' to select all.
Events get overlayed in the graph, you get a tooltip with the summary when hovering over it and when you click on it you go to the details page.
In order to make developing/testing this easier, there are also some new functions that generate data, so you can see a plot for whatever date you want without having real data.
- 306. By Lucio Torre
-
merged with trunk
- 307. By Lucio Torre
-
removed dirt.
Aman Gupta (tmm1) wrote : | # |
I really like the new test functions. Can you add some docs for http://
Also we should document the new dependency on django.tagging in requirements.txt and in the docs somewhere.
Aman Gupta (tmm1) wrote : | # |
As discussed on IRC, it would be nice to expose these new events to the server-side graphing api as well. Probably as a new function: events(tag, tag, ...), which could query the Events model and generate a TimeSeries to graph.
Lucio Torre (lucio.torre) wrote : | # |
updated js array comparison to include the one in the link
removed the autoescape for all event related templates
documented test functions
added django-tagging to requirements
added events(*tags) function for use with drawAsInfinite
fixed bug on events tooltips.
- 308. By Lucio Torre
-
merged with trunk
- 309. By Lucio Torre
-
fixes, documentation and new events functions
- 310. By Lucio Torre
-
more doc
chrismd (chrismd) wrote : | # |
Looks great, unfortunately I could not merge it into trunk because we haven't upgraded the repo to 2a format yet. The diff however applied cleanly as a patch, which I just committed to trunk.
DiegoV (diego-varese) wrote : | # |
Looks like your code does not work with a MySQL backend:
Dieter P (dieter-plaetinck) wrote : | # |
IMHO managing annotated events has nothing do with time series data; I feel like trying to bolt that logic onto graphite is a bit awkward and requires a lot of code additions/changes precisely because this is a different problem domain.
Based on that realization I wrote a small tool for annotated event management (with similar input/output API's as graphite): http://
Preview Diff
1 | === modified file 'docs/install.rst' |
2 | --- docs/install.rst 2011-03-21 20:22:09 +0000 |
3 | +++ docs/install.rst 2011-08-23 21:18:40 +0000 |
4 | @@ -14,11 +14,13 @@ |
5 | * Python 2.4 or greater (2.6+ recommended) |
6 | * `Pycairo <http://www.cairographics.org/pycairo/>`_ |
7 | * `Django <http://www.djangoproject.com/>`_ 1.0 or greater |
8 | +* `django-tagging <http://code.google.com/p/django-tagging/>`_ 0.3.1 |
9 | * A json module, if you're using Python2.6 this comes standard. With 2.4 you should |
10 | install `simplejson <http://pypi.python.org/pypi/simplejson/>`_ |
11 | * A Django-supported database module (sqlite comes standard with Python 2.6) |
12 | * `Twisted <http://twistedmatrix.com/>`_ 8.0 or greater (10.0+ recommended) |
13 | |
14 | + |
15 | Also both the Graphite webapp and Carbon require the whisper database library. |
16 | |
17 | There are also several optional dependencies, some of which are necessary for |
18 | |
19 | === modified file 'docs/who-is-using.rst' |
20 | --- docs/who-is-using.rst 2011-03-28 17:30:25 +0000 |
21 | +++ docs/who-is-using.rst 2011-08-23 21:18:40 +0000 |
22 | @@ -8,5 +8,6 @@ |
23 | * `Etsy <http://www.etsy.com/>`_ (see http://codeascraft.etsy.com/2010/12/08/track-every-release/) |
24 | * `Google <http://google-opensource.blogspot.com/2010/09/get-ready-to-rocksteady.html>`_ (opensource Rocksteady project) |
25 | * `Media Temple <http://mediatemple.net/>`_ |
26 | +* `Canonical <http://www.canonical.com>`_ |
27 | |
28 | And many more, I still need to collect some links... |
29 | |
30 | === modified file 'requirements.txt' |
31 | --- requirements.txt 2011-08-04 22:49:45 +0000 |
32 | +++ requirements.txt 2011-08-23 21:18:40 +0000 |
33 | @@ -31,5 +31,6 @@ |
34 | python-memcached==1.47 |
35 | txAMQP==0.4 |
36 | simplejson==2.1.6 |
37 | +django-tagging==0.3.1 |
38 | http://cairographics.org/releases/py2cairo-1.8.10.tar.gz |
39 | ./whisper |
40 | |
41 | === modified file 'setup.py' |
42 | --- setup.py 2011-08-20 05:25:28 +0000 |
43 | +++ setup.py 2011-08-23 21:18:40 +0000 |
44 | @@ -51,10 +51,13 @@ |
45 | 'graphite.metrics', |
46 | 'graphite.dashboard', |
47 | 'graphite.graphlot', |
48 | + 'graphite.events', |
49 | 'graphite.thirdparty', |
50 | 'graphite.thirdparty.pytz', |
51 | ], |
52 | - package_data={'graphite' : ['templates/*', 'local_settings.py.example', 'render/graphTemplates.conf']}, |
53 | + package_data={'graphite' : |
54 | + ['templates/*', 'local_settings.py.example', |
55 | + 'render/graphTemplates.conf']}, |
56 | scripts=glob('bin/*'), |
57 | data_files=webapp_content.items() + storage_dirs + conf_files, |
58 | **setup_kwargs |
59 | |
60 | === modified file 'webapp/content/js/jquery.graphite.js' |
61 | --- webapp/content/js/jquery.graphite.js 2011-07-10 23:30:06 +0000 |
62 | +++ webapp/content/js/jquery.graphite.js 2011-08-23 21:18:40 +0000 |
63 | @@ -1,4 +1,38 @@ |
64 | (function( $ ) { |
65 | + |
66 | + function arrays_equal(array1, array2) { |
67 | + if (array1 == null || array2 == null) { |
68 | + return false; |
69 | + } |
70 | + var temp = new Array(); |
71 | + if ( (!array1[0]) || (!array2[0]) ) { // If either is not an array |
72 | + return false; |
73 | + } |
74 | + if (array1.length != array2.length) { |
75 | + return false; |
76 | + } |
77 | + // Put all the elements from array1 into a "tagged" array |
78 | + for (var i=0; i<array1.length; i++) { |
79 | + key = (typeof array1[i]) + "~" + array1[i]; |
80 | + // Use "typeof" so a number 1 isn't equal to a string "1". |
81 | + if (temp[key]) { temp[key]++; } else { temp[key] = 1; } |
82 | + // temp[key] = # of occurrences of the value (so an element could appear multiple times) |
83 | + } |
84 | + // Go through array2 - if same tag missing in "tagged" array, not equal |
85 | + for (var i=0; i<array2.length; i++) { |
86 | + key = (typeof array2[i]) + "~" + array2[i]; |
87 | + if (temp[key]) { |
88 | + if (temp[key] == 0) { return false; } else { temp[key]--; } |
89 | + // Subtract to keep track of # of appearances in array2 |
90 | + } else { // Key didn't appear in array1, arrays are not equal. |
91 | + return false; |
92 | + } |
93 | + } |
94 | + // If we get to this point, then every generated key in array1 showed up the exact same |
95 | + // number of times in array2, so the arrays are equal. |
96 | + return true; |
97 | + } |
98 | + |
99 | $.fn.editable_in_place = function(callback) { |
100 | var editable = $(this); |
101 | if (editable.length > 1) { |
102 | @@ -6,14 +40,14 @@ |
103 | } |
104 | |
105 | var editing = false; |
106 | - |
107 | + |
108 | editable.bind('click', function () { |
109 | var $element = this; |
110 | |
111 | if (editing == true) return; |
112 | |
113 | editing = true; |
114 | - |
115 | + |
116 | var $edit = $('<input type="text" class="edit_in_place" value="' + editable.text() + '"/>'); |
117 | |
118 | $edit.css({'height' : editable.height(), 'width' : editable.width()}); |
119 | @@ -57,9 +91,10 @@ |
120 | var latestPosition = null; |
121 | var autocompleteoptions = { |
122 | minChars: 0, |
123 | - selectFirst: false, |
124 | - }; |
125 | - |
126 | + selectFirst: false |
127 | + }; |
128 | + var markings = []; |
129 | + |
130 | var parse_incoming = function(incoming_data) { |
131 | var result = []; |
132 | var start = incoming_data.start; |
133 | @@ -77,7 +112,7 @@ |
134 | }; |
135 | }; |
136 | |
137 | - |
138 | + |
139 | var render = function () { |
140 | var lines = [] |
141 | for (i in graph_lines) { |
142 | @@ -94,20 +129,20 @@ |
143 | |
144 | $.extend(xaxismode, xaxisranges); |
145 | $.extend(yaxismode, yaxisranges); |
146 | - |
147 | + |
148 | plot = $.plot($("#graph"), |
149 | lines, |
150 | { |
151 | xaxis: xaxismode, |
152 | yaxis: yaxismode, |
153 | - grid: { hoverable: true, }, |
154 | + grid: { hoverable: true, markings: markings }, |
155 | selection: { mode: "xy" }, |
156 | legend: { show: true, container: graph.find("#legend") }, |
157 | crosshair: { mode: "x" }, |
158 | } |
159 | ); |
160 | |
161 | - |
162 | + |
163 | for (i in lines) { |
164 | lines[i] = $.extend({}, lines[i]); |
165 | lines[i].label = null; |
166 | @@ -174,7 +209,7 @@ |
167 | if (!updateLegendTimeout) |
168 | updateLegendTimeout = setTimeout(updateLegend, 50); |
169 | }); |
170 | - |
171 | + |
172 | function showTooltip(x, y, contents) { |
173 | $('<div id="tooltip">' + contents + '</div>').css( { |
174 | position: 'absolute', |
175 | @@ -191,7 +226,7 @@ |
176 | var previousPoint = null; |
177 | $("#graph").bind("plothover", function (event, pos, item) { |
178 | if (item) { |
179 | - if (previousPoint != item.datapoint) { |
180 | + if ( !arrays_equal(previousPoint, item.datapoint)) { |
181 | previousPoint = item.datapoint; |
182 | |
183 | $("#tooltip").remove(); |
184 | @@ -201,13 +236,38 @@ |
185 | showTooltip(item.pageX, item.pageY, |
186 | item.series.label + " = " + y); |
187 | } |
188 | - } |
189 | - else { |
190 | - $("#tooltip").remove(); |
191 | - previousPoint = null; |
192 | + } else { |
193 | + calc_distance = function(mark, event) { |
194 | + mark_where = plot.pointOffset({ x: mark.xaxis.from, y: 0}); |
195 | + d = plot.offset().left + mark_where.left - event.pageX - plot.getPlotOffset().left; |
196 | + return d*d; |
197 | + } |
198 | + distance = undefined; |
199 | + winner = null; |
200 | + for (marki in markings) { |
201 | + mark = markings[marki]; |
202 | + dist = calc_distance(mark, pos); |
203 | + if (distance == undefined || distance > dist) { |
204 | + distance = dist; |
205 | + winner = mark; |
206 | + } |
207 | + } |
208 | + if (distance < 20) { |
209 | + if (!arrays_equal(previousPoint,[winner])) { |
210 | + previousPoint = [winner] |
211 | + $("#tooltip").remove(); |
212 | + showTooltip(pos.pageX-20, pos.pageY-20, winner.text); |
213 | + } |
214 | + } else { |
215 | + if (previousPoint != null) { |
216 | + previousPoint = null; |
217 | + $("#tooltip").remove(); |
218 | + } |
219 | + |
220 | + } |
221 | } |
222 | }); |
223 | - |
224 | + |
225 | $("#overview").bind("plotselected", function (event, ranges) { |
226 | xaxisranges = { min: ranges.xaxis.from, max: ranges.xaxis.to }; |
227 | yaxisranges = { min: ranges.yaxis.from, max: ranges.yaxis.to }; |
228 | @@ -231,6 +291,7 @@ |
229 | var metric = $(this); |
230 | update_metric_row(metric); |
231 | }); |
232 | + get_events(graph.find("#eventdesc")) |
233 | render(); |
234 | } |
235 | |
236 | @@ -245,9 +306,14 @@ |
237 | url = url + '&target=' + series; |
238 | } |
239 | } |
240 | + events = graph.find("#eventdesc").val(); |
241 | + if (events != "") { |
242 | + url = url + "&events=" + events; |
243 | + } |
244 | + |
245 | return url; |
246 | } |
247 | - |
248 | + |
249 | var build_when = function () { |
250 | var when = ''; |
251 | var from = graph.find("#from").text(); |
252 | @@ -265,6 +331,15 @@ |
253 | return 'rawdata?'+when+'&target='+series; |
254 | } |
255 | |
256 | + var build_url_events = function (tags) { |
257 | + when = build_when() |
258 | + if (tags == "*") { |
259 | + return '/events/get_data?'+when |
260 | + } else { |
261 | + return '/events/get_data?'+when+'&tags='+tags; |
262 | + } |
263 | + } |
264 | + |
265 | var update_metric_row = function(metric_row) { |
266 | var metric = $(metric_row); |
267 | var metric_name = metric.find(".metricname").text(); |
268 | @@ -290,9 +365,45 @@ |
269 | } |
270 | }); |
271 | |
272 | - |
273 | - } |
274 | - |
275 | + |
276 | + } |
277 | + |
278 | + var get_events = function(events_text) { |
279 | + if (events_text.val() == "") { |
280 | + events_text.removeClass("ajaxworking"); |
281 | + events_text.removeClass("ajaxerror"); |
282 | + markings = []; |
283 | + render(); |
284 | + } else { |
285 | + events_text.addClass("ajaxworking"); |
286 | + $.ajax({ |
287 | + url: build_url_events(events_text.val()), |
288 | + success: function(req_data) { |
289 | + events_text.removeClass("ajaxerror"); |
290 | + events_text.removeClass("ajaxworking"); |
291 | + markings = []; |
292 | + for (i in req_data) { |
293 | + row = req_data[i]; |
294 | + markings.push({ |
295 | + color: '#000', |
296 | + lineWidth: 1, |
297 | + xaxis: { from: row.when*1000, to: row.when*1000 }, |
298 | + text:'<a href="/events/'+row.id+'/">'+row.what+'<a>' |
299 | + }); |
300 | + } |
301 | + render(); |
302 | + }, |
303 | + error: function(req, status, err) { |
304 | + events_text.removeClass("ajaxworking"); |
305 | + events_text.addClass("ajaxerror"); |
306 | + render(); |
307 | + } |
308 | + }); |
309 | + } |
310 | + |
311 | + } |
312 | + |
313 | + |
314 | // configure the date boxes |
315 | graph.find('#from').editable_in_place( |
316 | function(editable, value) { |
317 | @@ -300,7 +411,7 @@ |
318 | recalculate_all(); |
319 | } |
320 | ); |
321 | - |
322 | + |
323 | |
324 | graph.find('#until').editable_in_place( |
325 | function(editable, value) { |
326 | @@ -309,7 +420,7 @@ |
327 | } |
328 | ); |
329 | |
330 | - graph.find('#update').bind('click', |
331 | + graph.find('#update').bind('click', |
332 | function() { |
333 | recalculate_all(); |
334 | } |
335 | @@ -318,11 +429,11 @@ |
336 | graph.find('#clearzoom').bind('click', |
337 | clear_zoom |
338 | ); |
339 | - |
340 | + |
341 | // configure metricrows |
342 | var setup_row = function (metric) { |
343 | var metric_name = metric.find('.metricname').text(); |
344 | - |
345 | + |
346 | metric.find('.metricname').editable_in_place( |
347 | function(editable, value) { |
348 | delete graph_lines[$(editable).text()]; |
349 | @@ -335,7 +446,7 @@ |
350 | metric.remove(); |
351 | render(); |
352 | }); |
353 | - |
354 | + |
355 | metric.find('.yaxis').bind('click', function() { |
356 | if ($(this).text() == "one") { |
357 | $(this).text("two"); |
358 | @@ -346,7 +457,7 @@ |
359 | render(); |
360 | }); |
361 | } |
362 | - |
363 | + |
364 | graph.find('.metricrow').each(function() { |
365 | setup_row($(this)); |
366 | }); |
367 | @@ -375,9 +486,23 @@ |
368 | }); |
369 | }); |
370 | |
371 | + // configure new metric input |
372 | + graph.find('#eventdesc').each(function () { |
373 | + var edit = $(this); |
374 | + edit.keydown(function(e) { |
375 | + if(e.which===13) { // on enter |
376 | + // add row |
377 | + edit.blur(); |
378 | + get_events(edit); |
379 | + } |
380 | + }); |
381 | + }); |
382 | + |
383 | + |
384 | // get data |
385 | recalculate_all(); |
386 | }); |
387 | }; |
388 | - |
389 | + |
390 | })( jQuery ); |
391 | + |
392 | |
393 | === modified file 'webapp/graphite/browser/urls.py' |
394 | --- webapp/graphite/browser/urls.py 2009-10-19 06:20:01 +0000 |
395 | +++ webapp/graphite/browser/urls.py 2011-08-23 21:18:40 +0000 |
396 | @@ -19,5 +19,5 @@ |
397 | ('^search/?$', 'search'), |
398 | ('^mygraph/?$', 'myGraphLookup'), |
399 | ('^usergraph/?$', 'userGraphLookup'), |
400 | - ('', 'browser'), |
401 | + ('^$', 'browser'), |
402 | ) |
403 | |
404 | === added directory 'webapp/graphite/events' |
405 | === added file 'webapp/graphite/events/__init__.py' |
406 | === added file 'webapp/graphite/events/models.py' |
407 | --- webapp/graphite/events/models.py 1970-01-01 00:00:00 +0000 |
408 | +++ webapp/graphite/events/models.py 2011-08-23 21:18:40 +0000 |
409 | @@ -0,0 +1,49 @@ |
410 | +import time |
411 | + |
412 | +from django.db import models |
413 | +from django.contrib import admin |
414 | + |
415 | +import tagging.fields |
416 | + |
417 | + |
418 | +class Event(models.Model): |
419 | + class Admin: pass |
420 | + |
421 | + when = models.DateTimeField() |
422 | + what = models.CharField(max_length=255) |
423 | + data = models.TextField(blank=True) |
424 | + tags = tagging.fields.TagField(default="") |
425 | + |
426 | + def get_tags(self): |
427 | + return Tag.objects.get_for_object(self) |
428 | + |
429 | + def __str__(self): |
430 | + return "%s: %s" % (self.when, self.what) |
431 | + |
432 | + @staticmethod |
433 | + def find_events(time_from=None, time_until=None, tags=None): |
434 | + query = Event.objects.all() |
435 | + |
436 | + if time_from is not None: |
437 | + query = query.filter(when__gte=time_from) |
438 | + |
439 | + if time_until is not None: |
440 | + query = query.filter(when__lte=time_until) |
441 | + |
442 | + if tags is not None: |
443 | + for tag in tags: |
444 | + query = query.filter(tags__iregex=r'\b%s\b' % tag) |
445 | + |
446 | + result = list(query.order_by("when")) |
447 | + return result |
448 | + |
449 | + def as_dict(self): |
450 | + return dict( |
451 | + when=self.when, |
452 | + what=self.what, |
453 | + data=self.data, |
454 | + tags=self.tags, |
455 | + id=self.id, |
456 | + ) |
457 | + |
458 | +admin.site.register(Event) |
459 | \ No newline at end of file |
460 | |
461 | === added file 'webapp/graphite/events/urls.py' |
462 | --- webapp/graphite/events/urls.py 1970-01-01 00:00:00 +0000 |
463 | +++ webapp/graphite/events/urls.py 2011-08-23 21:18:40 +0000 |
464 | @@ -0,0 +1,21 @@ |
465 | +"""Copyright 2008 Orbitz WorldWide |
466 | + |
467 | +Licensed under the Apache License, Version 2.0 (the "License"); |
468 | +you may not use this file except in compliance with the License. |
469 | +You may obtain a copy of the License at |
470 | + |
471 | + http://www.apache.org/licenses/LICENSE-2.0 |
472 | + |
473 | + Unless required by applicable law or agreed to in writing, software |
474 | + distributed under the License is distributed on an "AS IS" BASIS, |
475 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
476 | + See the License for the specific language governing permissions and |
477 | + limitations under the License.""" |
478 | + |
479 | +from django.conf.urls.defaults import * |
480 | + |
481 | +urlpatterns = patterns('graphite.events.views', |
482 | + ('^get_data?$', 'get_data'), |
483 | + (r'(?P<event_id>\d+)/$', 'detail'), |
484 | + ('^$', 'view_events'), |
485 | +) |
486 | |
487 | === added file 'webapp/graphite/events/views.py' |
488 | --- webapp/graphite/events/views.py 1970-01-01 00:00:00 +0000 |
489 | +++ webapp/graphite/events/views.py 2011-08-23 21:18:40 +0000 |
490 | @@ -0,0 +1,77 @@ |
491 | +import datetime |
492 | +import time |
493 | + |
494 | +import simplejson |
495 | + |
496 | +from django.http import HttpResponse |
497 | +from django.shortcuts import render_to_response, get_object_or_404 |
498 | + |
499 | +from graphite.events import models |
500 | +from graphite.render.attime import parseATTime |
501 | + |
502 | + |
503 | +def to_timestamp(dt): |
504 | + return time.mktime(dt.timetuple()) |
505 | + |
506 | + |
507 | +class EventEncoder(simplejson.JSONEncoder): |
508 | + def default(self, obj): |
509 | + if isinstance(obj, datetime.datetime): |
510 | + return to_timestamp(obj) |
511 | + return simplejson.JSONEncoder.default(self, obj) |
512 | + |
513 | + |
514 | +def view_events(request): |
515 | + if request.method == "GET": |
516 | + context = dict(events=fetch(request)) |
517 | + return render_to_response("events.html", context) |
518 | + else: |
519 | + return post_event(request) |
520 | + |
521 | +def detail(request, event_id): |
522 | + e = get_object_or_404(models.Event, pk=event_id) |
523 | + context = dict(event=e) |
524 | + return render_to_response("event.html", context) |
525 | + |
526 | + |
527 | +def post_event(request): |
528 | + if request.method == 'POST': |
529 | + event = simplejson.loads(request.raw_post_data) |
530 | + assert isinstance(event, dict) |
531 | + |
532 | + values = {} |
533 | + values["what"] = event["what"] |
534 | + values["tags"] = event.get("tags", None) |
535 | + values["when"] = datetime.datetime.fromtimestamp( |
536 | + event.get("when", time.time())) |
537 | + if "data" in event: |
538 | + values["data"] = event["data"] |
539 | + |
540 | + e = models.Event(**values) |
541 | + e.save() |
542 | + |
543 | + return HttpResponse(status=200) |
544 | + else: |
545 | + return HttpResponse(status=405) |
546 | + |
547 | +def get_data(request): |
548 | + return HttpResponse(simplejson.dumps(fetch(request), cls=EventEncoder), |
549 | + mimetype="application/json") |
550 | + |
551 | +def fetch(request): |
552 | + if request.GET.get("from", None) is not None: |
553 | + time_from = parseATTime(request.GET["from"]) |
554 | + else: |
555 | + time_from = datetime.datetime.fromtimestamp(0) |
556 | + |
557 | + if request.GET.get("until", None) is not None: |
558 | + time_until = parseATTime(request.GET["until"]) |
559 | + else: |
560 | + time_until = datetime.datetime.now() |
561 | + |
562 | + tags = request.GET.get("tags", None) |
563 | + if tags is not None: |
564 | + tags = request.GET.get("tags").split(" ") |
565 | + |
566 | + return [x.as_dict() for x in |
567 | + models.Event.find_events(time_from, time_until, tags=tags)] |
568 | |
569 | === modified file 'webapp/graphite/graphlot/views.py' |
570 | --- webapp/graphite/graphlot/views.py 2011-07-12 07:37:01 +0000 |
571 | +++ webapp/graphite/graphlot/views.py 2011-08-23 21:18:40 +0000 |
572 | @@ -20,7 +20,9 @@ |
573 | |
574 | untiltime = request.GET.get('until', "-0hour") |
575 | fromtime = request.GET.get('from', "-24hour") |
576 | - context = dict(metric_list=metrics, fromtime=fromtime, untiltime=untiltime) |
577 | + events = request.GET.get('events', "") |
578 | + context = dict(metric_list=metrics, fromtime=fromtime, untiltime=untiltime, |
579 | + events=events) |
580 | return render_to_response("graphlot.html", context) |
581 | |
582 | def get_data(request): |
583 | @@ -45,6 +47,7 @@ |
584 | raise Http404 |
585 | return HttpResponse(simplejson.dumps(result), mimetype="application/json") |
586 | |
587 | + |
588 | def find_metric(request): |
589 | """Autocomplete helper on metric names.""" |
590 | try: |
591 | |
592 | === modified file 'webapp/graphite/render/functions.py' |
593 | --- webapp/graphite/render/functions.py 2011-08-10 18:25:01 +0000 |
594 | +++ webapp/graphite/render/functions.py 2011-08-23 21:18:40 +0000 |
595 | @@ -17,11 +17,17 @@ |
596 | URL parameters to change the data being graphed in some way. |
597 | """ |
598 | |
599 | -from graphite.render.datalib import TimeSeries, timestamp |
600 | -from graphite.render.attime import parseTimeOffset |
601 | +import datetime |
602 | from itertools import izip |
603 | import math |
604 | import re |
605 | +import random |
606 | +import time |
607 | + |
608 | +from graphite.render.datalib import fetchData, TimeSeries, timestamp |
609 | +from graphite.render.attime import parseTimeOffset |
610 | + |
611 | +from graphite.events import models |
612 | |
613 | #Utility functions |
614 | def safeSum(values): |
615 | @@ -1360,6 +1366,141 @@ |
616 | return results |
617 | |
618 | |
619 | +def timeFunction(requestContext, name): |
620 | + """ |
621 | + Short Alias: time() |
622 | + |
623 | + Just returns the timestamp for each X value. T |
624 | + |
625 | + Example: |
626 | + |
627 | + .. code-block:: none |
628 | + |
629 | + &target=time("The.time.series") |
630 | + |
631 | + This would create a series named "The.time.series" that contains in Y the same |
632 | + value (in seconds) as X. |
633 | + |
634 | + """ |
635 | + |
636 | + step = 60 |
637 | + delta = datetime.timedelta(seconds=step) |
638 | + when = requestContext["startTime"] |
639 | + values = [] |
640 | + |
641 | + while when < requestContext["endTime"]: |
642 | + values.append(time.mktime(when.timetuple())) |
643 | + when += delta |
644 | + |
645 | + return [TimeSeries(name, |
646 | + time.mktime(requestContext["startTime"].timetuple()), |
647 | + time.mktime(requestContext["endTime"].timetuple()), |
648 | + step, values)] |
649 | + |
650 | + |
651 | +def sinFunction(requestContext, name, amplitude=1): |
652 | + """ |
653 | + Short Alias: sin() |
654 | + |
655 | + Just returns the sine of the current time. he optional amplitude parameter |
656 | + changes the amplitude of the wave. |
657 | + |
658 | + Example: |
659 | + |
660 | + .. code-block:: none |
661 | + |
662 | + &target=sin("The.time.series", 2) |
663 | + |
664 | + This would create a series named "The.time.series" that contains sin(x)*2. |
665 | + """ |
666 | + step = 60 |
667 | + delta = datetime.timedelta(seconds=step) |
668 | + when = requestContext["startTime"] |
669 | + values = [] |
670 | + |
671 | + while when < requestContext["endTime"]: |
672 | + values.append(math.sin(time.mktime(when.timetuple()))*amplitude) |
673 | + when += delta |
674 | + |
675 | + return [TimeSeries(name, |
676 | + time.mktime(requestContext["startTime"].timetuple()), |
677 | + time.mktime(requestContext["endTime"].timetuple()), |
678 | + step, values)] |
679 | + |
680 | +def randomWalkFunction(requestContext, name): |
681 | + """ |
682 | + Short Alias: randomWalk() |
683 | + |
684 | + Returns a random walk starting at 0. This is great for testing when there is |
685 | + no real data in whisper. |
686 | + |
687 | + Example: |
688 | + |
689 | + .. code-block:: none |
690 | + |
691 | + &target=randomWalk("The.time.series") |
692 | + |
693 | + This would create a series named "The.time.series" that contains points where |
694 | + x(t) == x(t-1)+random()-0.5, and x(0) == 0. |
695 | + """ |
696 | + step = 60 |
697 | + delta = datetime.timedelta(seconds=step) |
698 | + when = requestContext["startTime"] |
699 | + values = [] |
700 | + current = 0 |
701 | + while when < requestContext["endTime"]: |
702 | + values.append(current) |
703 | + current += random.random() - 0.5 |
704 | + when += delta |
705 | + |
706 | + return [TimeSeries(name, |
707 | + time.mktime(requestContext["startTime"].timetuple()), |
708 | + time.mktime(requestContext["endTime"].timetuple()), |
709 | + step, values)] |
710 | + |
711 | +def events(requestContext, *tags): |
712 | + """ |
713 | + Returns the number of events at this point in time. Usable with |
714 | + drawAsInfinite. |
715 | + |
716 | + Example: |
717 | + |
718 | + .. code-block:: none |
719 | + |
720 | + &target=events("tag-one", "tag-two") |
721 | + &target=events("*") |
722 | + |
723 | + Returns all events tagged as "tag-one" and "tag-two" and the second one |
724 | + returns all events. |
725 | + """ |
726 | + step = 60 |
727 | + name = "events(" + ", ".join(tags) + ")" |
728 | + delta = datetime.timedelta(seconds=step) |
729 | + when = requestContext["startTime"] |
730 | + values = [] |
731 | + current = 0 |
732 | + if tags == ("*",): |
733 | + tags = None |
734 | + events = models.Event.find_events(requestContext["startTime"], |
735 | + requestContext["endTime"], tags=tags) |
736 | + eventsp = 0 |
737 | + |
738 | + while when < requestContext["endTime"]: |
739 | + count = 0 |
740 | + if events: |
741 | + while eventsp < len(events) and events[eventsp].when >= when \ |
742 | + and events[eventsp].when < (when + delta): |
743 | + count += 1 |
744 | + eventsp += 1 |
745 | + |
746 | + values.append(count) |
747 | + when += delta |
748 | + |
749 | + return [TimeSeries(name, |
750 | + time.mktime(requestContext["startTime"].timetuple()), |
751 | + time.mktime(requestContext["endTime"].timetuple()), |
752 | + step, values)] |
753 | + |
754 | def pieAverage(requestContext, series): |
755 | return safeDiv(safeSum(series),safeLen(series)) |
756 | |
757 | @@ -1437,6 +1578,17 @@ |
758 | 'exclude' : exclude, |
759 | 'constantLine' : constantLine, |
760 | 'threshold' : threshold, |
761 | + |
762 | + # test functions |
763 | + 'time': timeFunction, |
764 | + "sin": sinFunction, |
765 | + "randomWalk": randomWalkFunction, |
766 | + 'timeFunction': timeFunction, |
767 | + "sinFunction": sinFunction, |
768 | + "randomWalkFunction": randomWalkFunction, |
769 | + |
770 | + #events |
771 | + 'events': events, |
772 | } |
773 | |
774 | |
775 | |
776 | === modified file 'webapp/graphite/settings.py' |
777 | --- webapp/graphite/settings.py 2011-07-26 05:16:59 +0000 |
778 | +++ webapp/graphite/settings.py 2011-08-23 21:18:40 +0000 |
779 | @@ -170,10 +170,12 @@ |
780 | 'graphite.account', |
781 | 'graphite.dashboard', |
782 | 'graphite.whitelist', |
783 | + 'graphite.events', |
784 | 'django.contrib.auth', |
785 | 'django.contrib.sessions', |
786 | 'django.contrib.admin', |
787 | 'django.contrib.contenttypes', |
788 | + 'tagging', |
789 | ) |
790 | |
791 | AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend'] |
792 | |
793 | === modified file 'webapp/graphite/templates/browserHeader.html' |
794 | --- webapp/graphite/templates/browserHeader.html 2011-07-10 23:30:06 +0000 |
795 | +++ webapp/graphite/templates/browserHeader.html 2011-08-23 21:18:40 +0000 |
796 | @@ -97,6 +97,7 @@ |
797 | <tr><td style='font-size: smaller;'> |
798 | User Interface: <a href="/dashboard/" target="_top">Dashboard</a> | |
799 | <a href="/graphlot/" target="_top">flot (experimental)</a> | |
800 | + <a href="/events/" target="_top">events (experimental)</a> | |
801 | <a href="/cli/" target="_top">Command-Line (deprecated)</a> |
802 | </td></tr> |
803 | |
804 | |
805 | === added file 'webapp/graphite/templates/event.html' |
806 | --- webapp/graphite/templates/event.html 1970-01-01 00:00:00 +0000 |
807 | +++ webapp/graphite/templates/event.html 2011-08-23 21:18:40 +0000 |
808 | @@ -0,0 +1,30 @@ |
809 | +<html> |
810 | + <head> |
811 | + <title>{{event.what}}</title> |
812 | + <link rel="stylesheet" type="text/css" href="/content/css/table.css" /> |
813 | + <style type="text/css"> |
814 | + body { |
815 | + font-family: sans-serif; |
816 | + font-size: 16px; |
817 | + margin: 50px; |
818 | + max-width: 1200px; |
819 | + } |
820 | + </style> |
821 | + |
822 | + |
823 | + </head> |
824 | + <body> |
825 | + <div id="title" style="text-align:center"> |
826 | + <h1>{{event.what}}</h1> |
827 | + </div> |
828 | + <div class="graphite"> |
829 | + <div id="main" > |
830 | + <table class="styledtable" width=100%> |
831 | + <tr><td>when</td><td>{{event.when|date:"H:m:s D d M Y" }}</td></tr> |
832 | + <tr><td>tags</td><td>{{event.tags}}</td></tr> |
833 | + <tr><td>data</td><td>{{event.data}}</td></tr> |
834 | + </table> |
835 | + </div> |
836 | + </div> |
837 | + </body> |
838 | +</html> |
839 | \ No newline at end of file |
840 | |
841 | === added file 'webapp/graphite/templates/events.html' |
842 | --- webapp/graphite/templates/events.html 1970-01-01 00:00:00 +0000 |
843 | +++ webapp/graphite/templates/events.html 2011-08-23 21:18:40 +0000 |
844 | @@ -0,0 +1,42 @@ |
845 | +<html> |
846 | + <head> |
847 | + <title>Events</title> |
848 | + <link rel="stylesheet" type="text/css" href="../content/css/table.css" /> |
849 | + <style type="text/css"> |
850 | + body { |
851 | + font-family: sans-serif; |
852 | + font-size: 16px; |
853 | + margin: 50px; |
854 | + max-width: 1200px; |
855 | + } |
856 | + </style> |
857 | + |
858 | + |
859 | + </head> |
860 | + <body> |
861 | + <div id="title" style="text-align:center"> |
862 | + <h1>graphite events</h1> |
863 | + </div> |
864 | + <div class="graphite"> |
865 | + <div id="main" > |
866 | + {% if events %} |
867 | + <table class="styledtable" width=100%> |
868 | + <tr><th>when</th><th>what</th><th>tags</th></tr> |
869 | + {% for event in events %} |
870 | + <tr> |
871 | + <td>{{event.when|date:"H:m:s D d M Y" }}</td> |
872 | + <td><a href="/events/{{event.id}}/">{{event.what}}</a></td> |
873 | + <td>{{event.tags}}</td> |
874 | + </tr> |
875 | + {% endfor %} |
876 | + {% else %} |
877 | + <br/>No events. Add events using |
878 | + <a href="/admin/events/event/">the admin interface</a> or by posting |
879 | + (eg, curl -X POST http://localhost:8000/events/ -d |
880 | + '{"what": "Something Interesting"}') |
881 | + {% endif %} |
882 | + </table> |
883 | + </div> |
884 | + </div> |
885 | + </body> |
886 | +</html> |
887 | |
888 | === modified file 'webapp/graphite/templates/graphlot.html' |
889 | --- webapp/graphite/templates/graphlot.html 2010-10-30 21:58:12 +0000 |
890 | +++ webapp/graphite/templates/graphlot.html 2011-08-23 21:18:40 +0000 |
891 | @@ -1,5 +1,3 @@ |
892 | -{% autoescape off %} |
893 | - |
894 | <html> |
895 | <head> |
896 | <title>Graphlot</title> |
897 | @@ -59,6 +57,11 @@ |
898 | <br/> |
899 | |
900 | <table id="rowlist" class="styledtable" style="float:left"> |
901 | + <tr><th style="width:750px">events</th></tr> |
902 | + <tr id="eventsrow"><td><input class="event_tags" style="width:600px" type="text" id="eventdesc" value="{{events}}"/></td></tr> |
903 | + </table> |
904 | + |
905 | + <table id="rowlist" class="styledtable" style="float:left"> |
906 | <tr><th style="width:750px">metric</th><th>y-axis</th></tr> |
907 | {% for metric in metric_list %} |
908 | <tr class="metricrow"> |
909 | @@ -94,6 +97,4 @@ |
910 | |
911 | </div> |
912 | </body> |
913 | -</html> |
914 | - |
915 | -{% endautoescape %} |
916 | \ No newline at end of file |
917 | +</html> |
918 | \ No newline at end of file |
919 | |
920 | === modified file 'webapp/graphite/urls.py' |
921 | --- webapp/graphite/urls.py 2011-07-12 07:37:01 +0000 |
922 | +++ webapp/graphite/urls.py 2011-08-23 21:18:40 +0000 |
923 | @@ -35,7 +35,8 @@ |
924 | ('^dashboard/?', include('graphite.dashboard.urls')), |
925 | ('^whitelist/?', include('graphite.whitelist.urls')), |
926 | ('^content/(?P<path>.*)$', 'django.views.static.serve', {'document_root' : settings.CONTENT_DIR}), |
927 | - ('^graphlot/?', include('graphite.graphlot.urls')), |
928 | + ('graphlot/', include('graphite.graphlot.urls')), |
929 | + ('^events/', include('graphite.events.urls')), |
930 | ('', 'graphite.browser.views.browser'), |
931 | ) |
932 |
Looks pretty ok to me. A few comments, but none of them blockers:
[1] arrays_equal has a few issues the way it's implemented, but should work given how it's used. There's a really nice array comparison implementation here: http:// www.breakingpar .com/bkp/ home.nsf/ 0/87256B2800151 93F87256BFB0077 DFFD
[2] I'm worried about the use of 'autoescape off' here, since basically you could inject some javascript through post_event handler and have it rendered unsanitized into the template, but Lucio pointed out there's more templates already using 'autoescape off' in the tree. Probably deserves some checking all around. It doesn't seem like removing it from those newly introduced templates would break them though.