# # views.py -- Views for the log app # # Copyright (c) 2009 Christian Hammond # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # import calendar import datetime import logging import os import re import time from urllib import urlencode from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required from django.http import Http404 from django.shortcuts import render_to_response from django.template.context import RequestContext from django.utils.translation import ugettext as _ LEVELS = ( (logging.DEBUG, 'debug', _('Debug')), (logging.INFO, 'info', _('Info')), (logging.WARNING, 'warning', _('Warning')), (logging.ERROR, 'error', _('Error')), (logging.CRITICAL, 'critical', _('Critical')), ) # Matches the default timestamp format in the logging module. TIMESTAMP_FMT = '%Y-%m-%d %H:%M:%S' LOG_LINE_RE = re.compile( r'(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - ' r'(?PDEBUG|INFO|WARNING|ERROR|CRITICAL) - ' r'(?P.*)') def parse_timestamp(format, timestamp_str): """Utility function to parse a timestamp into a datetime.datetime. Python 2.5 and up have datetime.strptime, but Python 2.4 does not, so we roll our own as per the documentation. If passed a timestamp_str of None, we will return None as a convenience. """ if not timestamp_str: return None return datetime.datetime(*time.strptime(timestamp_str, format)[0:6]) def build_query_string(request, params): """Builds a query string that includes the specified parameters along with those that were passed to the page. params is a dictionary. """ query_parts = [] for key, value in request.GET.iteritems(): if key not in params: query_parts.append(urlencode({ key: value })) for key, value in params.iteritems(): if value is not None: query_parts.append(urlencode({ key: value })) return '?' + '&'.join(query_parts) def iter_log_lines(from_timestamp, to_timestamp, requested_levels): """Generator that iterates over lines in a log file, yielding the yielding information about the lines.""" log_filename = os.path.join(settings.LOGGING_DIRECTORY, settings.LOGGING_NAME + '.log') line_info = None try: fp = open(log_filename, 'r') except IOError: # We'd log this, but it'd do very little good in practice. # It would only appear on the console when using the development # server, but production users would never see anything. So, # just return gracefully. We'll show an empty log, which is # about accurate. return for line in fp.xreadlines(): line = line.rstrip() m = LOG_LINE_RE.match(line) if m: if line_info: # We have a fully-formed log line and this new line isn't # part of it, so yield it now. yield line_info line_info = None timestamp_str = m.group('timestamp') level = m.group('level') message = m.group('message') if not requested_levels or level.lower() in requested_levels: timestamp = parse_timestamp(TIMESTAMP_FMT, timestamp_str.split(',')[0]) timestamp_date = timestamp.date() if ((from_timestamp and from_timestamp > timestamp_date) or (to_timestamp and to_timestamp < timestamp_date)): continue line_info = (timestamp, level, message) elif line_info: line_info = (line_info[0], line_info[1], line_info[2] + "\n" + line) if line_info: yield line_info fp.close() def get_log_filtersets(request, requested_levels, from_timestamp, to_timestamp): """Returns the filtersets that will be used in the log view.""" logger = logging.getLogger('') level_filters = [ { 'name': _('All'), 'url': build_query_string(request, {'levels': None}), 'selected': len(requested_levels) == 0, } ] + [ { 'name': label_name, 'url': build_query_string(request, {'levels': level_name}), 'selected': level_name in requested_levels, } for level_id, level_name, label_name in LEVELS if logger.isEnabledFor(level_id) ] from_timestamp_str = request.GET.get('from', None) to_timestamp_str = request.GET.get('to', None) today = datetime.date.today() today_str = today.strftime('%Y-%m-%d') one_week_ago = today - datetime.timedelta(days=7) one_week_ago_str = one_week_ago.strftime('%Y-%m-%d') month_range = calendar.monthrange(today.year, today.month) this_month_begin_str = today.strftime('%Y-%m-01') this_month_end_str = today.strftime('%Y-%m-') + str(month_range[1]) date_filters = [ { 'name': _('Any date'), 'url': build_query_string(request, { 'from': None, 'to': None, }), 'selected': from_timestamp_str is None and to_timestamp_str is None, }, { 'name': _('Today'), 'url': build_query_string(request, { 'from': today_str, 'to': today_str, }), 'selected': from_timestamp_str == today_str and to_timestamp_str == today_str, }, { 'name': _('Past 7 days'), 'url': build_query_string(request, { 'from': one_week_ago_str, 'to': today_str, }), 'selected': from_timestamp_str == one_week_ago_str and to_timestamp_str == today_str, }, { 'name': _('This month'), 'url': build_query_string(request, { 'from': this_month_begin_str, 'to': this_month_end_str, }), 'selected': from_timestamp_str == this_month_begin_str and to_timestamp_str == this_month_end_str, }, ] return ( (_("By date"), date_filters), (_("By level"), level_filters), ) @staff_member_required def server_log(request, template_name='log/log.html'): """Displays the server log.""" # First check if logging is even configured. If it's not, just return # a 404. if (not getattr(settings, "LOGGING_ENABLED", False) or not getattr(settings, "LOGGING_DIRECTORY", None)): raise Http404() requested_levels = [] # Get the list of levels to show. if 'levels' in request.GET: requested_levels = request.GET.get('levels').split(',') # Get the timestamp ranges. from_timestamp = parse_timestamp('%Y-%m-%d', request.GET.get('from')) to_timestamp = parse_timestamp('%Y-%m-%d', request.GET.get('to')) if from_timestamp: from_timestamp = from_timestamp.date() if to_timestamp: to_timestamp = to_timestamp.date() # Get the filters to show. filtersets = get_log_filtersets(request, requested_levels, from_timestamp, to_timestamp) # Grab the lines from the log file. log_lines = iter_log_lines(from_timestamp, to_timestamp, requested_levels) # Figure out the sorting sort_type = request.GET.get('sort', 'asc') if sort_type == 'asc': reverse_sort_type = 'desc' else: reverse_sort_type = 'asc' log_lines = reversed(list(log_lines)) response = render_to_response(template_name, RequestContext(request, { 'log_lines': log_lines, 'filtersets': filtersets, 'sort_url': build_query_string(request, {'sort': reverse_sort_type}), 'sort_type': sort_type, })) return response