← Back to team overview

graphite-dev team mailing list archive

[Merge] lp:~lucio.torre/graphite/add-events into lp:graphite

 

Lucio Torre has proposed merging lp:~lucio.torre/graphite/add-events into lp:graphite.

Requested reviews:
  graphite-dev (graphite-dev)

For more details, see:
https://code.launchpad.net/~lucio.torre/graphite/add-events/+merge/69142

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/


-- 
https://code.launchpad.net/~lucio.torre/graphite/add-events/+merge/69142
Your team graphite-dev is requested to review the proposed merge of lp:~lucio.torre/graphite/add-events into lp:graphite.
=== modified file 'setup.py'
--- setup.py	2011-07-20 06:19:14 +0000
+++ setup.py	2011-07-25 18:13:24 +0000
@@ -40,6 +40,7 @@
   license='Apache Software License 2.0',
   description='Enterprise scalable realtime graphing',
   package_dir={'' : 'webapp'},
+<<<<<<< TREE
   packages=[
     'graphite',
     'graphite.account',
@@ -55,6 +56,14 @@
     'graphite.thirdparty.pytz',
   ],
   package_data={'graphite' : ['templates/*', 'local_settings.py.example', 'render/graphTemplates.conf']},
+=======
+  packages=['graphite', 'graphite.account', 'graphite.browser', 
+            'graphite.cli', 'graphite.composer', 'graphite.render', 
+            'graphite.whitelist', 'graphite.metrics', 'graphite.graphlot',
+            'graphite.events'],
+  package_data={'graphite' : ['templates/*', 'local_settings.py.example', 
+                              'render/graphTemplates.conf']},
+>>>>>>> MERGE-SOURCE
   scripts=glob('bin/*'),
   data_files=webapp_content.items() + storage_dirs + conf_files,
   **setup_kwargs

=== modified file 'webapp/content/js/jquery.graphite.js'
--- webapp/content/js/jquery.graphite.js	2011-07-10 23:30:06 +0000
+++ webapp/content/js/jquery.graphite.js	2011-07-25 18:13:24 +0000
@@ -1,4 +1,23 @@
 (function( $ ) {
+
+    function arrays_equal(a,b) {
+        if (a == b) {
+            return true;
+        }
+        if (a == null || b == null) {
+            return false;
+        }
+        if (a.length != b.length) {
+            return false;
+        }
+        for ( i in a ) {
+            if (a[i] != b[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     $.fn.editable_in_place = function(callback) {
         var editable = $(this);
         if (editable.length > 1) {
@@ -6,14 +25,14 @@
         }
 
         var editing = false;
-        
+
         editable.bind('click', function () {
             var $element = this;
 
             if (editing == true) return;
 
             editing = true;
-            
+
             var $edit = $('<input type="text" class="edit_in_place" value="' + editable.text() + '"/>');
 
             $edit.css({'height' : editable.height(), 'width' : editable.width()});
@@ -57,9 +76,10 @@
             var latestPosition = null;
             var autocompleteoptions = {
                         minChars: 0,
-                        selectFirst: false,
-                    };
-                    
+                        selectFirst: false
+            };
+            var markings = [];
+
             var parse_incoming = function(incoming_data) {
                 var result = [];
                 var start = incoming_data.start;
@@ -77,7 +97,7 @@
                 };
             };
 
-            
+
             var render = function () {
                 var lines = []
                 for (i in graph_lines) {
@@ -94,20 +114,20 @@
 
                 $.extend(xaxismode, xaxisranges);
                 $.extend(yaxismode, yaxisranges);
-                
+
                 plot = $.plot($("#graph"),
                     lines,
                     {
                         xaxis: xaxismode,
                         yaxis: yaxismode,
-                        grid: { hoverable: true, },
+                        grid: { hoverable: true, markings: markings },
                         selection: { mode: "xy" },
                         legend: { show: true, container: graph.find("#legend") },
                         crosshair: { mode: "x" },
                     }
                 );
 
-                
+
                 for (i in lines) {
                     lines[i] = $.extend({}, lines[i]);
                     lines[i].label = null;
@@ -174,7 +194,7 @@
                 if (!updateLegendTimeout)
                     updateLegendTimeout = setTimeout(updateLegend, 50);
             });
-            
+
             function showTooltip(x, y, contents) {
                 $('<div id="tooltip">' + contents + '</div>').css( {
                     position: 'absolute',
@@ -191,7 +211,7 @@
             var previousPoint = null;
             $("#graph").bind("plothover", function (event, pos, item) {
                 if (item) {
-                    if (previousPoint != item.datapoint) {
+                    if ( !arrays_equal(previousPoint, item.datapoint)) {
                         previousPoint = item.datapoint;
 
                         $("#tooltip").remove();
@@ -201,13 +221,38 @@
                         showTooltip(item.pageX, item.pageY,
                                     item.series.label + " = " + y);
                     }
-                }
-                else {
-                    $("#tooltip").remove();
-                    previousPoint = null;
+                } else {
+                    calc_distance = function(mark, event) {
+                        mark_where = plot.pointOffset({ x: mark.xaxis.from, y: 0});
+                        d = plot.offset().left + mark_where.left - event.pageX - plot.getPlotOffset().left;
+                        return d*d;
+                    }
+                    distance = undefined;
+                    winner = null;
+                    for (marki in markings) {
+                        mark = markings[marki];
+                        dist = calc_distance(mark, pos);
+                        if (distance == undefined || distance > dist) {
+                            distance = dist;
+                            winner = mark;
+                        }
+                    }
+                    if (distance < 20) {
+                        if (!arrays_equal(previousPoint,[winner])) {
+                            previousPoint = [winner]
+                            $("#tooltip").remove();
+                            showTooltip(pos.pageX-20, pos.pageY-20, mark.text);
+                        }
+                    } else {
+                        if (previousPoint != null) {
+                            previousPoint = null;
+                            $("#tooltip").remove();
+                        }
+
+                    }
                 }
             });
-            
+
             $("#overview").bind("plotselected", function (event, ranges) {
                 xaxisranges = { min: ranges.xaxis.from, max: ranges.xaxis.to };
                 yaxisranges = { min: ranges.yaxis.from, max: ranges.yaxis.to };
@@ -231,6 +276,7 @@
                     var metric = $(this);
                     update_metric_row(metric);
                 });
+                get_events(graph.find("#eventdesc"))
                 render();
             }
 
@@ -245,9 +291,14 @@
                         url = url + '&target=' + series;
                     }
                 }
+                events = graph.find("#eventdesc").val();
+                if (events != "") {
+                    url = url + "&events=" + events;
+                }
+
                 return url;
             }
-                
+
             var build_when = function () {
                 var when = '';
                 var from  = graph.find("#from").text();
@@ -265,6 +316,15 @@
                 return 'rawdata?'+when+'&target='+series;
             }
 
+            var build_url_events = function (tags) {
+                when = build_when()
+                if (tags == "*") {
+                    return '/events/get_data?'+when
+                } else {
+                    return '/events/get_data?'+when+'&tags='+tags;
+                }
+            }
+
             var update_metric_row = function(metric_row) {
                 var metric = $(metric_row);
                 var metric_name = metric.find(".metricname").text();
@@ -290,9 +350,45 @@
                     }
                 });
 
-                
-            }
-            
+
+            }
+
+            var get_events = function(events_text) {
+                if (events_text.val() == "") {
+                    events_text.removeClass("ajaxworking");
+                    events_text.removeClass("ajaxerror");
+                    markings = [];
+                    render();
+                } else {
+                    events_text.addClass("ajaxworking");
+                    $.ajax({
+                        url: build_url_events(events_text.val()),
+                        success: function(req_data) {
+                            events_text.removeClass("ajaxerror");
+                            events_text.removeClass("ajaxworking");
+                            markings = [];
+                            for (i in req_data) {
+                                row = req_data[i];
+                                markings.push({
+                                    color: '#000',
+                                    lineWidth: 1,
+                                    xaxis: { from: row.when*1000, to: row.when*1000 },
+                                    text:'<a href="/events/'+row.id+'/">'+row.what+'<a>'
+                                });
+                            }
+                            render();
+                        },
+                        error: function(req, status, err) {
+                            events_text.removeClass("ajaxworking");
+                            events_text.addClass("ajaxerror");
+                            render();
+                        }
+                    });
+                }
+
+            }
+
+
             // configure the date boxes
             graph.find('#from').editable_in_place(
                 function(editable, value) {
@@ -300,7 +396,7 @@
                     recalculate_all();
                 }
             );
-            
+
 
             graph.find('#until').editable_in_place(
                 function(editable, value) {
@@ -309,7 +405,7 @@
                 }
             );
 
-            graph.find('#update').bind('click', 
+            graph.find('#update').bind('click',
                 function() {
                     recalculate_all();
                 }
@@ -318,11 +414,11 @@
             graph.find('#clearzoom').bind('click',
                 clear_zoom
             );
-            
+
             // configure metricrows
             var setup_row = function (metric) {
                 var metric_name = metric.find('.metricname').text();
-                
+
                 metric.find('.metricname').editable_in_place(
                     function(editable, value) {
                         delete graph_lines[$(editable).text()];
@@ -335,7 +431,7 @@
                     metric.remove();
                     render();
                 });
-                
+
                 metric.find('.yaxis').bind('click', function() {
                     if ($(this).text() == "one") {
                         $(this).text("two");
@@ -346,7 +442,7 @@
                     render();
                 });
             }
-            
+
             graph.find('.metricrow').each(function() {
                 setup_row($(this));
             });
@@ -375,9 +471,26 @@
                 });
             });
 
+            // configure new metric input
+            graph.find('#eventdesc').each(function () {
+                var edit = $(this);
+                edit.keydown(function(e) {
+                    if(e.which===13) { // on enter
+                        // add row
+                        edit.blur();
+                        get_events(edit);
+                    }
+                });
+            });
+
+
             // get data
             recalculate_all();
         });
     };
+<<<<<<< TREE
     
 })( jQuery );
+=======
+
+})( jQuery );>>>>>>> MERGE-SOURCE

=== modified file 'webapp/graphite/browser/urls.py'
--- webapp/graphite/browser/urls.py	2009-10-19 06:20:01 +0000
+++ webapp/graphite/browser/urls.py	2011-07-25 18:13:24 +0000
@@ -19,5 +19,5 @@
   ('^search/?$', 'search'),
   ('^mygraph/?$', 'myGraphLookup'),
   ('^usergraph/?$', 'userGraphLookup'),
-  ('', 'browser'),
+  ('^$', 'browser'),
 )

=== added directory 'webapp/graphite/events'
=== added file 'webapp/graphite/events/__init__.py'
=== added file 'webapp/graphite/events/models.py'
--- webapp/graphite/events/models.py	1970-01-01 00:00:00 +0000
+++ webapp/graphite/events/models.py	2011-07-25 18:13:24 +0000
@@ -0,0 +1,49 @@
+import time
+
+from django.db import models
+from django.contrib import admin
+
+import tagging.fields
+
+    
+class Event(models.Model):
+    class Admin: pass
+    
+    when = models.DateTimeField()
+    what = models.CharField(max_length=255)
+    data = models.TextField(blank=True)
+    tags = tagging.fields.TagField(default="")
+    
+    def get_tags(self):
+        return Tag.objects.get_for_object(self)
+        
+    def __str__(self):
+        return "%s: %s" % (self.when, self.what)
+    
+    @staticmethod
+    def find_events(time_from=None, time_until=None, tags=None):
+        query = Event.objects.all()
+        
+        if time_from is not None:
+            query = query.filter(when__gte=time_from)
+            
+        if time_until is not None:
+            query = query.filter(when__lte=time_until)
+        
+        if tags is not None:
+            for tag in tags:
+                query = query.filter(tags__iregex=r'\b%s\b' % tag)
+        
+        result = list(query.order_by("when"))
+        return result
+    
+    def as_dict(self):
+        return dict(
+            when=self.when,
+            what=self.what,
+            data=self.data,
+            tags=self.tags,
+            id=self.id,
+        )
+    
+admin.site.register(Event)
\ No newline at end of file

=== added file 'webapp/graphite/events/tests.py'
--- webapp/graphite/events/tests.py	1970-01-01 00:00:00 +0000
+++ webapp/graphite/events/tests.py	2011-07-25 18:13:24 +0000
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+

=== added file 'webapp/graphite/events/urls.py'
--- webapp/graphite/events/urls.py	1970-01-01 00:00:00 +0000
+++ webapp/graphite/events/urls.py	2011-07-25 18:13:24 +0000
@@ -0,0 +1,21 @@
+"""Copyright 2008 Orbitz WorldWide
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License."""
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('graphite.events.views',
+  ('^get_data?$', 'get_data'),
+  (r'(?P<event_id>\d+)/$', 'detail'),
+  ('^$', 'view_events'),
+)

=== added file 'webapp/graphite/events/views.py'
--- webapp/graphite/events/views.py	1970-01-01 00:00:00 +0000
+++ webapp/graphite/events/views.py	2011-07-25 18:13:24 +0000
@@ -0,0 +1,77 @@
+import datetime
+import time
+
+import simplejson
+
+from django.http import HttpResponse
+from django.shortcuts import render_to_response, get_object_or_404
+
+from graphite.events import models
+from graphite.render.attime import parseATTime
+
+
+def to_timestamp(dt):
+    return time.mktime(dt.timetuple())
+
+
+class EventEncoder(simplejson.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, datetime.datetime):
+            return to_timestamp(obj)
+        return simplejson.JSONEncoder.default(self, obj)
+
+
+def view_events(request):
+    if request.method == "GET":
+        context = dict(events=fetch(request))
+        return render_to_response("events.html", context)
+    else:
+        return post_event(request)
+
+def detail(request, event_id):
+    e = get_object_or_404(models.Event, pk=event_id)
+    context = dict(event=e)
+    return render_to_response("event.html", context)
+
+
+def post_event(request):
+    if request.method == 'POST':
+        event = simplejson.loads(request.raw_post_data)
+        assert isinstance(event, dict)
+
+        values = {}
+        values["what"] = event["what"]
+        values["tags"] = event.get("tags", None)
+        values["when"] = datetime.datetime.fromtimestamp(
+            event.get("when", time.time()))
+        if "data" in event:
+            values["data"] = event["data"]
+
+        e = models.Event(**values)
+        e.save()
+
+        return HttpResponse(status=200)
+    else:
+        return HttpResponse(status=405)
+
+def get_data(request):
+    return HttpResponse(simplejson.dumps(fetch(request), cls=EventEncoder),
+                        mimetype="application/json")
+
+def fetch(request):
+    if request.GET.get("from", None) is not None:
+        time_from = parseATTime(request.GET["from"])
+    else:
+        time_from = datetime.datetime.fromtimestamp(0)
+
+    if request.GET.get("until", None) is not None:
+        time_until = parseATTime(request.GET["until"])
+    else:
+        time_until = datetime.datetime.now()
+
+    tags = request.GET.get("tags", None)
+    if tags is not None:
+        tags = request.GET.get("tags").split(" ")
+
+    return [x.as_dict() for x in
+            models.Event.find_events(time_from, time_until, tags=tags)]

=== modified file 'webapp/graphite/graphlot/views.py'
--- webapp/graphite/graphlot/views.py	2011-07-12 07:37:01 +0000
+++ webapp/graphite/graphlot/views.py	2011-07-25 18:13:24 +0000
@@ -20,7 +20,9 @@
 
     untiltime = request.GET.get('until', "-0hour")
     fromtime = request.GET.get('from', "-24hour")
-    context = dict(metric_list=metrics, fromtime=fromtime, untiltime=untiltime)
+    events = request.GET.get('events', "")
+    context = dict(metric_list=metrics, fromtime=fromtime, untiltime=untiltime,
+                   events=events)
     return render_to_response("graphlot.html", context)
 
 def get_data(request):
@@ -45,6 +47,7 @@
         raise Http404
     return HttpResponse(simplejson.dumps(result), mimetype="application/json")
 
+
 def find_metric(request):
     """Autocomplete helper on metric names."""
     try:

=== modified file 'webapp/graphite/render/evaluator.py'
--- webapp/graphite/render/evaluator.py	2010-10-25 18:09:41 +0000
+++ webapp/graphite/render/evaluator.py	2011-07-25 18:13:24 +0000
@@ -24,7 +24,9 @@
     return fetchData(requestContext, tokens.pathExpression)
 
   elif tokens.call:
+    print tokens.call
     func = SeriesFunctions[tokens.call.func]
+    print tokens.call.func, func
     args = [evaluateTokens(requestContext, arg) for arg in tokens.call.args]
     return func(requestContext, *args)
 

=== modified file 'webapp/graphite/render/functions.py'
--- webapp/graphite/render/functions.py	2011-07-12 05:45:26 +0000
+++ webapp/graphite/render/functions.py	2011-07-25 18:13:24 +0000
@@ -12,11 +12,20 @@
 See the License for the specific language governing permissions and
 limitations under the License."""
 
+<<<<<<< TREE
 from graphite.render.datalib import TimeSeries, timestamp
 from graphite.render.attime import parseTimeOffset
+=======
+import datetime
+>>>>>>> MERGE-SOURCE
 from itertools import izip
 import math
 import re
+import random
+import time
+
+from graphite.render.datalib import fetchData, TimeSeries, timestamp
+from graphite.render.attime import parseTimeOffset
 
 #Utility functions
 def safeSum(values):
@@ -1300,6 +1309,67 @@
   return results
 
 
+def timeFunction(requestContext, name):
+  step = 60
+  delta = datetime.timedelta(seconds=step)
+  when = requestContext["startTime"]
+  values = []
+  
+  while when < requestContext["endTime"]:
+    values.append(time.mktime(when.timetuple()))
+    when += delta
+
+  return [TimeSeries(name,
+            time.mktime(requestContext["startTime"].timetuple()),
+            time.mktime(requestContext["endTime"].timetuple()),
+            step, values)]
+  
+def constantFunction(requestContext, name, value):
+  step = 60
+  delta = datetime.timedelta(seconds=step)
+  when = requestContext["startTime"]
+  values = []
+  
+  while when < requestContext["endTime"]:
+    values.append(value)
+    when += delta
+
+  return [TimeSeries(name,
+            time.mktime(requestContext["startTime"].timetuple()),
+            time.mktime(requestContext["endTime"].timetuple()),
+            step, values)]
+
+def sinFunction(requestContext, name, amplitude=1):
+  step = 60
+  delta = datetime.timedelta(seconds=step)
+  when = requestContext["startTime"]
+  values = []
+  
+  while when < requestContext["endTime"]:
+    values.append(math.sin(time.mktime(when.timetuple()))*amplitude)
+    when += delta
+
+  return [TimeSeries(name,
+            time.mktime(requestContext["startTime"].timetuple()),
+            time.mktime(requestContext["endTime"].timetuple()),
+            step, values)]
+  
+def randomWalkFunction(requestContext, name):
+  step = 60
+  delta = datetime.timedelta(seconds=step)
+  when = requestContext["startTime"]
+  values = []
+  current = 0
+  while when < requestContext["endTime"]:
+    values.append(current)
+    current += random.random() - 0.5
+    when += delta
+
+  return [TimeSeries(name,
+            time.mktime(requestContext["startTime"].timetuple()),
+            time.mktime(requestContext["endTime"].timetuple()),
+            step, values)]
+  
 def pieAverage(requestContext, series):
   return safeDiv(safeSum(series),safeLen(series))
 
@@ -1377,6 +1447,12 @@
   'exclude' : exclude,
   'constantLine' : constantLine,
   'threshold' : threshold,
+  
+  # test functions
+  'time': timeFunction,
+  "constant": constantFunction,
+  "sin": sinFunction,
+  "randomWalk": randomWalkFunction,
 }
 
 

=== modified file 'webapp/graphite/settings.py'
--- webapp/graphite/settings.py	2011-07-20 20:00:50 +0000
+++ webapp/graphite/settings.py	2011-07-25 18:13:24 +0000
@@ -169,10 +169,12 @@
   'graphite.account',
   'graphite.dashboard',
   'graphite.whitelist',
+  'graphite.events',
   'django.contrib.auth',
   'django.contrib.sessions',
   'django.contrib.admin',
   'django.contrib.contenttypes',
+  'tagging',
 )
 
 AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']

=== added file 'webapp/graphite/templates/event.html'
--- webapp/graphite/templates/event.html	1970-01-01 00:00:00 +0000
+++ webapp/graphite/templates/event.html	2011-07-25 18:13:24 +0000
@@ -0,0 +1,34 @@
+{% autoescape off %}
+
+<html>
+  <head>
+    <title>{{event.what}}</title>
+    <link rel="stylesheet" type="text/css" href="/content/css/table.css" />
+    <style type="text/css">
+    body {
+        font-family: sans-serif;
+        font-size: 16px;
+        margin: 50px;
+        max-width: 1200px;
+    }
+    </style>
+
+
+    </head>
+    <body>
+        <div id="title" style="text-align:center">
+            <h1>{{event.what}}</h1>
+        </div>
+        <div class="graphite">
+            <div id="main" >
+              <table class="styledtable" width=100%>
+                <tr><td>when</td><td>{{event.when|date:"H:m:s D d M Y" }}</td></tr>
+                <tr><td>tags</td><td>{{event.tags}}</td></tr>
+                <tr><td>data</td><td>{{event.data}}</td></tr>
+              </table>
+            </div>
+        </div>
+    </body>
+</html>
+
+{% endautoescape %}
\ No newline at end of file

=== added file 'webapp/graphite/templates/events.html'
--- webapp/graphite/templates/events.html	1970-01-01 00:00:00 +0000
+++ webapp/graphite/templates/events.html	2011-07-25 18:13:24 +0000
@@ -0,0 +1,39 @@
+{% autoescape off %}
+
+<html>
+  <head>
+    <title>Events</title>
+    <link rel="stylesheet" type="text/css" href="../content/css/table.css" />
+    <style type="text/css">
+    body {
+        font-family: sans-serif;
+        font-size: 16px;
+        margin: 50px;
+        max-width: 1200px;
+    }
+    </style>
+
+
+    </head>
+    <body>
+        <div id="title" style="text-align:center">
+            <h1>graphite events</h1>
+        </div>
+        <div class="graphite">
+            <div id="main" >
+              <table class="styledtable" width=100%>
+                <tr><th>when</th><th>what</th><th>tags</th></tr>
+                {% for event in events %}
+                    <tr>
+                        <td>{{event.when|date:"H:m:s D d M Y" }}</td>
+                        <td><a href="/events/{{event.id}}/">{{event.what}}</a></td>
+                        <td>{{event.tags}}</td>
+                    </tr>
+                {% endfor %}
+              </table>
+            </div>
+        </div>
+    </body>
+</html>
+
+{% endautoescape %}
\ No newline at end of file

=== modified file 'webapp/graphite/templates/graphlot.html'
--- webapp/graphite/templates/graphlot.html	2010-10-30 21:58:12 +0000
+++ webapp/graphite/templates/graphlot.html	2011-07-25 18:13:24 +0000
@@ -59,6 +59,11 @@
                 <br/>
 
                 <table id="rowlist" class="styledtable"  style="float:left">
+                <tr><th  style="width:750px">events</th></tr>
+                <tr id="eventsrow"><td><input class="event_tags" style="width:600px" type="text" id="eventdesc" value="{{events}}"/></td></tr>
+                </table>
+                
+                <table id="rowlist" class="styledtable"  style="float:left">
                 <tr><th  style="width:750px">metric</th><th>y-axis</th></tr>
                 {% for metric in metric_list %}
                     <tr class="metricrow">

=== modified file 'webapp/graphite/urls.py'
--- webapp/graphite/urls.py	2011-07-12 07:37:01 +0000
+++ webapp/graphite/urls.py	2011-07-25 18:13:24 +0000
@@ -35,8 +35,14 @@
   ('^dashboard/?', include('graphite.dashboard.urls')),
   ('^whitelist/?', include('graphite.whitelist.urls')),
   ('^content/(?P<path>.*)$', 'django.views.static.serve', {'document_root' : settings.CONTENT_DIR}),
+<<<<<<< TREE
   ('^graphlot/?', include('graphite.graphlot.urls')),
   ('', 'graphite.browser.views.browser'),
+=======
+  ('', include('graphite.graphlot.urls')),
+  ('', include('graphite.browser.urls')),
+  ('^events/', include('graphite.events.urls')),
+>>>>>>> MERGE-SOURCE
 )
 
 handler500 = 'graphite.views.server_error'