Merge lp:~lucio.torre/graphite/add-events into lp:~graphite-dev/graphite/main

Proposed by Lucio Torre
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
Reviewer Review Type Date Requested Status
chrismd Approve
Sidnei da Silva (community) Approve
Review via email: mp+69142@code.launchpad.net

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://ubuntuone.com/p/16CJ/

You can add events from the command line using curl:
$ curl -X POST http://localhost:8000/events/ -d '{"what": "Something Interesting"}'

So its very easy to integrate with other tools.

You can see the list of events from: http://localhost:8000/events/ and get to the event details by clicking on its line. (eg, http://localhost:8000/events/4)

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.

See: http://ubuntuone.com/p/16CY/

To post a comment you must log in.
lp:~lucio.torre/graphite/add-events updated
306. By Lucio Torre

merged with trunk

307. By Lucio Torre

removed dirt.

Revision history for this message
Sidnei da Silva (sidnei) wrote :

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/87256B280015193F87256BFB0077DFFD

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

review: Approve
Revision history for this message
Aman Gupta (tmm1) wrote :

I really like the new test functions. Can you add some docs for http://readthedocs.org/docs/graphite/en/latest/functions.html

Also we should document the new dependency on django.tagging in requirements.txt and in the docs somewhere.

Revision history for this message
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.

Revision history for this message
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.

lp:~lucio.torre/graphite/add-events updated
308. By Lucio Torre

merged with trunk

309. By Lucio Torre

fixes, documentation and new events functions

310. By Lucio Torre

more doc

Revision history for this message
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.

review: Approve
Revision history for this message
DiegoV (diego-varese) wrote :

Looks like your code does not work with a MySQL backend:

https://bugs.launchpad.net/graphite/+bug/1068861

Revision history for this message
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://dieter.plaetinck.be/anthracite-event-database-enrich-monitoring-dashboards-visual-numerical-analysis-events-business-impact.html

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches