diff --git a/gracedb/api.py b/gracedb/api.py
index 11157c5a8a586e204064ad1f0502f6fce41b3935..6e4cd78e93325d3d2bf2712398eebd3464ef1dfc 100644
--- a/gracedb/api.py
+++ b/gracedb/api.py
@@ -8,7 +8,7 @@ from django.conf import settings
 
 import json
 
-from gracedb.models import Event, Group, EventLog, Slot
+from gracedb.models import Event, Group, EventLog, Slot, Tag
 from gracedb.views import create_label
 from translator import handle_uploaded_data
 
@@ -115,6 +115,7 @@ def eventToDict(event, columns=None, request=None):
             "filemeta" : reverse("filemeta", args=[graceid], request=request),
             "labels" : reverse("labels", args=[graceid], request=request),
             "self"  : reverse("event-detail", args=[graceid], request=request),
+            "tags"  : reverse("eventtag-list", args=[graceid], request=request),
             }
     # XXX Jam the slots in here? Could just have a list of slot names instead of
     # all these links.  But the links might be useful??
@@ -472,12 +473,16 @@ class EventLabel(APIView):
 # Janky serialization
 def eventLogToDict(log, n=None, request=None):
     # XXX Messy.  n should not be here but in the model.
+    taglist_uri = None
     if (n is None) and request:
         uri = request.build_absolute_uri()
     elif n is not None and request:
         uri = reverse("eventlog-detail",
                 args=[log.event.graceid(), n],
                 request=request)
+        taglist_uri = reverse("eventlogtag-list",
+                args=[log.event.graceid(), n],
+                request=request)
     else:
         uri = None
     return {
@@ -485,6 +490,7 @@ def eventLogToDict(log, n=None, request=None):
                 "created" : log.created,
                 "issuer"  : log.issuer.name,
                 "self"    : uri,
+                "tags"    : taglist_uri,
            }
 
 class EventLogList(APIView):
@@ -547,7 +553,254 @@ class EventLogDetail(APIView):
             return Response("Log Entry Not Found",
                     status=status.HTTP_404_NOT_FOUND)
         rv = event.eventlog_set.order_by("created").all()[int(n)]
-        return Response(eventLogToDict(rv, request=request))
+        # XXX I (Branson) put the n argument here.  Why not?  
+        # We might as well since we have it, right?
+        return Response(eventLogToDict(rv, n, request=request))
+
+#==================================================================
+# Tags
+
+
+def tagToDict(tag, columns=None, request=None, event=None, n=None):
+    """Convert a tag to a dictionary.
+       Output depends on the level of specificity.
+    """
+
+    rv = {}
+    rv['name'] = tag.name
+    rv['displayName'] = tag.displayName
+    if event:
+        if n:
+            # We want a link to the self only.  End of the line.
+            rv['links'] = {
+                            "self" : reverse("eventlogtag-detail",
+                                             args=[event.graceid(),n,tag.name],
+                                             request=request)
+                          }
+        else:
+            # Links to all log messages of the event with this tag.
+            rv['links'] = {
+                            "logs" : [reverse("eventlog-detail", 
+                                              args=[event.graceid(),log.getN()], 
+                                              request=request) 
+                                      for log in event.getLogsForTag(tag.name)],
+                            "self" : reverse("eventtag-detail",
+                                             args=[event.graceid(),tag.name],
+                                             request=request)
+                          }
+    else:
+        # Links to all events that have this tag.
+        rv['links'] = {
+                        "events" : [reverse("event-detail", 
+                                            args=[event.graceid()], 
+                                            request=request) 
+                                    for event in tag.getEvents()],
+                        "self"   : reverse("tag-detail",
+                                           args=[tag.name],
+                                           request=request)
+                      }
+    return rv
+
+class TagList(APIView):
+    """Tag List Resource
+    """
+    authentication_classes = (LigoAuthentication,)
+    permission_classes = (IsAuthenticated,)
+
+    def get(self, request):
+        # Return a list of links to all tag objects.
+        rv = {
+                'tags' : [ reverse("tag-detail", args=[tag.name],
+                                   request=request)
+                           for tag in Tag.objects.all() ]
+             }
+        return Response(rv)
+
+class TagDetail(APIView):
+    """Tag Detail Resource
+    """
+    authentication_classes = (LigoAuthentication,)
+    permission_classes = (IsAuthenticated,)
+
+    def get(self, request, tagname):
+        try:
+            tag = Tag.objects.filter(name=tagname)[0]
+        except Tag.DoesNotExist:
+            return Response("Tag not found.",
+                    status=status.HTTP_404_NOT_FOUND)
+        return Response(tagToDict(tag,request=request))
+
+class EventTagList(APIView):
+    """Event Tag List Resource
+    """
+    authentication_classes = (LigoAuthentication,)
+    permission_classes = (IsAuthenticated,)
+
+    def get(self, request, graceid):
+        # Return a list of links to all tags for this event.
+        try:
+            event = Event.getByGraceid(graceid)
+        except Event.DoesNotExist:
+            # XXX Real error message.
+            return Response("Event does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+
+        rv = {
+                'tags' : [ reverse("eventtag-detail",args=[graceid,
+                                   tag.name],
+                                   request=request)
+                           for tag in event.getAvailableTags()]
+             }
+
+        return Response(rv)
+
+class EventTagDetail(APIView):
+    """Event Tag List Resource
+    """
+    authentication_classes = (LigoAuthentication,)
+    permission_classes = (IsAuthenticated,)
+
+    def get(self, request, graceid, tagname):
+        try:
+            event = Event.getByGraceid(graceid)
+        except Event.DoesNotExist:
+            # XXX Real error message.
+            return Response("Event does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        try:
+            tag = Tag.objects.filter(name=tagname)[0]
+            rv = tagToDict(tag,event=event,request=request)
+            return Response(rv)
+        except Tag.DoesNotExist:
+            return Response("No such tag for event.",
+                    status=status.HTTP_404_NOT_FOUND)
+
+class EventLogTagList(APIView):
+    """Event Log Tag List Resource
+    """
+    authentication_classes = (LigoAuthentication,)
+    permission_classes = (IsAuthenticated,)
+
+    def get(self, request, graceid, n):
+        # Return a list of links to tags associated with a given log message
+        try:
+            event = Event.getByGraceid(graceid)
+            eventlog = event.eventlog_set.order_by("created").all()[int(n)]
+        except Event.DoesNotExist:
+            # XXX Real error message.
+            return Response("Event does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        except:
+            # XXX Real error message.
+            return Response("Log does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+
+        rv = {
+                'tags' : [ reverse("eventlogtag-detail",
+                                    args=[graceid, 
+                                    n, tag.name],
+                                    request=request)
+                           for tag in eventlog.tag_set.all()]
+             }
+
+        return Response(rv)
+
+class EventLogTagDetail(APIView):
+    """Event Log Tag Detail Resource
+    """
+    authentication_classes = (LigoAuthentication,)
+    permission_classes = (IsAuthenticated,)
+
+    def get(self, request, graceid, n, tagname):
+        try:
+            event = Event.getByGraceid(graceid)
+            eventlog = event.eventlog_set.order_by("created").all()[int(n)]
+        except Event.DoesNotExist:
+            # XXX Real error message.
+            return Response("Event does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        except:
+            # XXX Real error message.
+            return Response("Log does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        try:
+            tag = eventlog.tag_set.filter(name=tagname)[0]
+            # Serialize
+            return Response(tagToDict(tag,event=event,n=n,request=request))
+        except:
+            return Response("Tag not found.",status=status.HTTP_404_NOT_FOUND)
+
+    def put(self, request, graceid, n, tagname):
+        logger = logging.getLogger(__name__)
+        try:
+            event = Event.getByGraceid(graceid)
+            eventlog = event.eventlog_set.order_by("created").all()[int(n)]
+        except Event.DoesNotExist:
+            # XXX Real error message.
+            return Response("Event does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        except:
+            # XXX Real error message.
+            return Response("Log does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        try:
+            # Has this tag-eventlog relationship already been created? If so, kick out.
+            # Actually, adding the eventlog to the tag would not hurt anything--no
+            # duplicate entry would be made in the database.  However, we don't want
+            # an extra log entry, or a deceptive HTTP response (i.e., one telling the 
+            # client that the creation was sucessful when, in fact, the database
+            # was unchanged.
+            tag = eventlog.tag_set.filter(name=tagname)[0]
+            msg = "Log already has tag %s" % unicode(tag)
+            return Response(msg,status=status.HTTP_409_CONFLICT)
+        except:
+            # Look for the tag.  If it doesn't already exist, create it.
+            try:
+                tag = Tag.objects.filter(name=tagname)[0]
+            except:
+                displayName = request.DATA.get('filename')
+                tag = Tag(name=tagname, displayName=displayName)
+                tag.save()
+
+            # Now add the log message to this tag.
+            tag.eventlogs.add(eventlog)
+
+            # Create a log entry to document the tag creation.
+            msg = "Tagged message %s: %s " % (n, tagname)
+            logentry = EventLog(event=event,
+                               issuer=request.ligouser,
+                               comment=msg)
+            logentry.save()
+
+            return Response("Tag created.",status=status.HTTP_201_CREATED)
+
+    def delete(self, request, graceid, n, tagname):
+        try:
+            event = Event.getByGraceid(graceid)
+            eventlog = event.eventlog_set.order_by("created").all()[int(n)]
+        except Event.DoesNotExist:
+            # XXX Real error message.
+            return Response("Event does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        except:
+            # XXX Real error message.
+            return Response("Log does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        try:
+            tag = eventlog.tag_set.filter(name=tagname)[0]
+            tag.delete()
+            return Response("Tag deleted.",status=status.HTTP_200_OK)
+
+            # Create a log entry to document the tag creation.
+            msg = "Removed tag %s for message %s. " % (tagname, n)
+            logentry = EventLog(event=event,
+                               issuer=request.ligouser,
+                               comment=msg)
+            logentry.save()
+
+        except:
+            return Response("Tag not found.",status=status.HTTP_404_NOT_FOUND)
+
 
 #==================================================================
 # Root Resource
@@ -583,6 +836,17 @@ class GracedbRoot(APIView):
         slot = slot.replace("G1200", "{graceid}")
         slot = slot.replace("slotname", "{slotname}")
 
+        taglist = reverse("eventlogtag-list", args=["G1200", "0"], request=request)
+        taglist = taglist.replace("G1200", "{graceid}")
+        taglist = taglist.replace("0", "{n}")
+
+        tag = reverse("eventlogtag-detail", args=["G1200", "0", "tagname"], request=request)
+        tag = tag.replace("G1200", "{graceid}")
+        tag = tag.replace("0", "{n}")
+        tag = tag.replace("tagname", "{tagname}")
+
+        # XXX Need a template for the tag list?
+
         templates = {
                 "event-detail-template" : detail,
                 "event-log-template" : log,
@@ -590,6 +854,8 @@ class GracedbRoot(APIView):
                 "files-template" : files,
                 "filemeta-template" : filemeta,
                 "slot-template" : slot,
+                "tag-template" : tag,
+                "taglist-template" : taglist,
                 }
 
         return Response({
@@ -884,10 +1150,25 @@ class EventSlot(APIView):
             slot = Slot.objects.filter(event=event).filter(name=slotname)[0]
             slot.value = filename
             slot.save()
+            # Create a log entry to document the slot update.
+            msg = "Updated slot %s with file " % slotname
+            logentry = EventLog(event=event,
+                               issuer=request.ligouser,
+                               filename=tmpFilename,
+                               comment=msg)
+            logentry.save()
         except:
             # Create the slot.
             slot = Slot(event=event,name=slotname,value=filename)
             slot.save()
+            # Create a log entry to document the slot creation.
+            msg = "Created slot %s with file " % slotname
+            logentry = EventLog(event=event,
+                               issuer=request.ligouser,
+                               filename=tmpFilename,
+                               comment=msg)
+            logentry.save()
+
         return Response("Slot created or updated.",status=status.HTTP_201_CREATED)
 
     # Delete a slot.
@@ -909,5 +1190,12 @@ class EventSlot(APIView):
                     status=status.HTTP_404_NOT_FOUND)
 
         slot.delete()
+        # Create a log entry to document the slot destruction.
+        msg = "Deleted slot %s " % slotname
+        logentry = EventLog(event=event,
+                           issuer=request.ligouser,
+                           comment=msg)
+        logentry.save()
+
         return Response("Slot deleted.",status=status.HTTP_200_OK)
 
diff --git a/gracedb/models.py b/gracedb/models.py
index 0b19353a4dfea6dd0c4605ee295f3fa133c92ffc..2e33a9737f536c5dacffb40b32727cda0e6054ca 100644
--- a/gracedb/models.py
+++ b/gracedb/models.py
@@ -171,6 +171,25 @@ class Event(models.Model):
     def __unicode__(self):
         return self.graceid()
 
+    # Return a list of distinct tags associated with the log messages of this
+    # event.
+    def getAvailableTags(self):
+        tagset_list = [log.tag_set.all() for log in self.eventlog_set.all()]
+        taglist = []
+        for tagset in tagset_list:
+            for tag in tagset:
+                taglist.append(tag)
+        # Eliminate duplicates
+        return list(set(taglist))
+
+    def getLogsForTag(self,tagname):
+        loglist = []
+        for log in self.eventlog_set.all():
+            for tag in log.tag_set.all():
+                if tag.name==tagname:
+                    loglist.append(log)
+        return loglist
+
 class EventLog(models.Model):
     class Meta:
         ordering = ["-created"]
@@ -191,6 +210,16 @@ class EventLog(models.Model):
         # XXX hacky
         return self.filename and self.filename[-3:].lower() in ['png','gif','jpg']
 
+    def getN(self):
+        # XXX also hacky?
+        # I think it would still work if some logs were removed from the database.
+        logset = self.event.eventlog_set.order_by("created")
+        # XXX This actually evaluates the queryset.  This may be a problem if 
+        # there are a huge number of log messages for this event and they 
+        # take up a lot of memory
+        logset = list(logset)
+        return logset.index(self)
+
 class Labelling(models.Model):
     event = models.ForeignKey(Event)
     label = models.ForeignKey(Label)
@@ -238,8 +267,32 @@ class MultiBurstEvent(Event):
     ligo_angle       = models.FloatField(null=True)
     ligo_angle_sig   = models.FloatField(null=True)
 
-## Slots (user-defined event attributes)
+## Tags (user-defined log message attributes)
+class Tag(models.Model):
+    """Slot Model"""
+    # XXX Does the tag need to have a submitter column?
+    # No, because creating a tag will generate a log message.
+    # For the same reason, a timstamp is not necessary.
+    eventlogs   = models.ManyToManyField(EventLog)
+    name        = models.CharField(max_length=100)
+    displayName = models.CharField(max_length=200,null=True)
 
+    def __unicode__(self):
+        if self.displayName:
+            return self.displayName
+        else:
+            return self.name
+
+    def getEvents(self):
+        # XXX Any way of doing this with filters?
+        # We would need to filter for a non-null intersection of the 
+        # set of log messages in the event with the set of log 
+        # messages in the tag.
+        eventlist = [log.event for log in self.eventlogs.all()]
+        return list(set(eventlist))
+   
+
+## XXX Get rid of the slots.  Probably.
 class Slot(models.Model):
     """Slot Model"""
     # Does the slot need to have a submitter column?
diff --git a/gracedb/urls_rest.py b/gracedb/urls_rest.py
index d915fe9e4996cdfb41625b04da04a7d4318aa751..804298351e7b16103bc3c5c0f7ed78caa5fc00c0 100644
--- a/gracedb/urls_rest.py
+++ b/gracedb/urls_rest.py
@@ -5,6 +5,9 @@ from django.conf.urls.defaults import patterns, url
 from gracedb.api import GracedbRoot
 from gracedb.api import EventList, EventDetail
 from gracedb.api import EventLogList, EventLogDetail
+from gracedb.api import TagList, TagDetail
+from gracedb.api import EventTagList, EventTagDetail
+from gracedb.api import EventLogTagList, EventLogTagDetail
 from gracedb.api import EventSlot
 from gracedb.api import Files, FileMeta
 from gracedb.api import EventNeighbors, EventLabel
@@ -27,6 +30,20 @@ urlpatterns = patterns('gracedb.api',
     url (r'events/(?P<graceid>[GEHT]\d+)/log/(?P<n>\d+)$',
         EventLogDetail.as_view(), name='eventlog-detail'),
 
+    # Tag Resources
+    url (r'^tag/$', 
+        TagList.as_view(), name='tag-list'),
+    url (r'^tag/(?P<tagname>\w+)$', 
+        TagDetail.as_view(), name='tag-detail'),
+    url (r'events/(?P<graceid>[GEHT]\d+)/tag/$',
+        EventTagList.as_view(), name='eventtag-list'),
+    url (r'events/(?P<graceid>[GEHT]\d+)/tag/(?P<tagname>\w+)$',
+        EventTagDetail.as_view(), name='eventtag-detail'),
+    url (r'events/(?P<graceid>[GEHT]\d+)/log/(?P<n>\d+)/tag/$',
+        EventLogTagList.as_view(), name='eventlogtag-list'),
+    url (r'events/(?P<graceid>[GEHT]\d+)/log/(?P<n>\d+)/tag/(?P<tagname>\w+)$',
+        EventLogTagDetail.as_view(), name='eventlogtag-detail'),
+
     # Event File Resources
     # events/{graceid}/files/[{filename}[/{version}]]
     url (r'^events/(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$',