Review Board

beta

Add logging/profiling support to Djblets

Updated 4 months, 4 weeks ago

Christian Hammond Reviewers
trunk reviewboard
None Navi
This adds a new piece of Middleware to Djblets that provides logging and profiling functionality. Or to be more specific, this defines loggers for use in Python's native logging module.

On the first request, we create a logger for writing to a file, as defined by the settings of the project. If in DEBUG mode, we also define a logger to show all output on the console.

A third logger is used for writing profile information, if the LOGGING_ALLOW_PROFILING option is set to True and a ?profiling=1 parameter is passed to the URL. This can be useful during development to help optimize applications.

This also introduces a useful log_timed function that displays log messages for the beginning and end of an operation, shows the start and end times (using the native logging functionality), and then shows a warning or a critical log message depending  on how long the operation takes.
Tested all features with my upcoming corresponding change in Review Board.
/trunk/djblets/djblets/logging/__init__.py
New File
1
from datetime import datetime
2
import logging
3
4
5
class TimedLogInfo(object):
6
    """
7
    A utility class created by ``log_timed`` that handles the timed logging
8
    functionality and provides a way to end the timed logging operation.
9
    """
10
    def __init__(self, message, warning_at, critical_at, default_level,
11
                 log_beginning):
12
        self.message = message
13
        self.warning_at = warning_at
14
        self.critical_at = critical_at
15
        self.default_level = default_level
16
        self.start_time = datetime.now()
17
18
        if log_beginning:
19
            logging.log(self.default_level, "Begin: %s" % self.message)
20
21
    def done(self):
22
        """
23
        Stops the timed logging operation. The resulting time of the
24
        operation will be written to the log file. The log level depends
25
        on how long the operation takes.
26
        """
27
        delta = datetime.now() - self.start_time
28
        level = self.default_level
29
30
        if delta.seconds >= self.critical_at:
31
            level = logging.CRITICAL
32
        elif delta.seconds >= self.warning_at:
33
            level = logging.WARNING
34
35
        logging.log(self.default_level, "End: %s" % self.message)
36
        logging.log(level, "%s took %s.%s seconds" % (self.message,
37
                                                      delta.seconds,
38
                                                      delta.microseconds))
39
40
def log_timed(message, warning_at=5, critical_at=15,
41
              log_beginning=True, default_level=logging.DEBUG):
42
    """
43
    Times an operation, displaying a log message before and after the
44
    operation. The log level for the final log message containing the
45
    operation runtime will be based on the runtime, the ``warning_at`` and
46
    the ``critical_at`` parameters.
47
    """
48
    return TimedLogInfo(message, warning_at, critical_at, default_level,
49
                        log_beginning)
/trunk/djblets/djblets/logging/middleware.py
New File
1
from datetime import datetime
2
import logging
3
import os.path
4
import sys
5
6
from django.conf import settings
7
8
9
_logging_setup = False
10
_profile_log = None
11
12
13
class LoggingMiddleware(object):
14
    """
15
    A piece of middleware that sets up logging.
16
17
    This a few settings to configure.
18
19
    LOGGING_ENABLED
20
    ---------------
21
22
    Default: False
23
24
    Sets whether or not logging is enabled.
25
26
27
    LOGGING_DIRECTORY
28
    -----------------
29
30
    Default: None
31
32
    Specifies the directory that log files should be stored in.
33
    This directory must be writable by the process running Django.
34
35
36
    LOGGING_NAME
37
    ------------
38
39
    Default: None
40
41
    The name of the log files, excluding the extension and path. This will
42
    usually be the name of the website or web application. The file extension
43
    will be automatically appended when the file is written.
44
45
46
    LOGGING_ALLOW_PROFILING
47
    -----------------------
48
49
    Default: False
50
51
    Specifies whether or not code profiling is allowed. If True, visiting
52
    any page with a ``?profiling=1`` parameter in the URL will cause the
53
    request to be profiled and stored in a ``.prof`` file using the defined
54
    ``LOGGING_DIRECTORY`` and ``LOGGING_NAME``.
55
56
57
    LOGGING_LINE_FORMAT
58
    -------------------
59
60
    Default: "%(asctime)s - %(levelname)s - %(message)s"
61
62
    The format for lines in the log file. See Python's logging documentation
63
    for possible values in the format string.
64
65
66
    LOGGING_LEVEL
67
    -------------
68
69
    Default: "DEBUG"
70
71
    The minimum level to log. Possible values are ``DEBUG``, ``INFO``,
72
    ``WARNING``, ``ERROR`` and ``CRITICAL``.
73
    """
74
75
    DEFAULT_LOG_LEVEL = "DEBUG"
76
    DEFAULT_LINE_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
77
78
    def process_request(self, request):
79
        """
80
        Processes an incoming request. This will set up logging.
81
        """
82
        self.ensure_loggers()
83
84
    def process_view(self, request, callback, callback_args, callback_kwargs):
85
        """
86
        Handler for processing a view. This will run the profiler on the view
87
        if profiling is allowed in the settings and the user specified the
88
        profiling parameter on the URL.
89
        """
90
        if ('profiling' in request.GET and
91
            getattr(settings, "LOGGING_ALLOW_PROFILING", False)):
92
            import cProfile
93
            self.profiler = cProfile.Profile()
94
            args = (request,) + callback_args
95
            return self.profiler.runcall(callback, *args, **callback_kwargs)
96
97
    def process_response(self, request, response):
98
        """
99
        Handler for processing a response. Dumps the profiling information
100
        to the profile log file.
101
        """
102
        if ('profiling' in request.GET and
103
            getattr(settings, "LOGGING_ALLOW_PROFILING", False)):
104
105
            self.ensure_profile_logger()
106
107
            from cStringIO import StringIO
108
            self.profiler.create_stats()
109
110
            # Capture the stats
111
            out = StringIO()
112
            old_stdout, sys.stdout = sys.stdout, out
113
            self.profiler.print_stats(1)
114
            sys.stdout = old_stdout
115
116
            _profile_log.log(logging.INFO,
117
                             "Profiling results for %s (HTTP %s):",
118
                             request.path, request.method)
119
            _profile_log.log(logging.INFO, out.getvalue().strip())
120
121
        return response
122
123
    def ensure_loggers(self):
124
        """
125
        Sets up the main loggers, if they haven't already been set up.
126
        """
127
        global _logging_setup
128
129
        if _logging_setup:
130
            return
131
132
        enabled = getattr(settings, 'LOGGING_ENABLED', False)
133
        log_directory = getattr(settings, 'LOGGING_DIRECTORY', None)
134
        log_name = getattr(settings, 'LOGGING_NAME', None)
135
136
        if not enabled or not log_directory or not log_name:
137
            return
138
139
        log_level_name = getattr(settings, 'LOGGING_LEVEL',
140
                                 self.DEFAULT_LOG_LEVEL)
141
        log_level = logging.getLevelName(log_level_name)
142
        format_str = getattr(settings, 'LOGGING_LINE_FORMAT',
143
                             self.DEFAULT_LINE_FORMAT)
144
145
        log_path = os.path.join(log_directory, log_name + ".log")
146
147
        logging.basicConfig(
148
            level=log_level,
149
            format=format_str,
150
            filename=log_path,
151
            filemode='a'
152
        )
153
154
        if settings.DEBUG:
155
            # In DEBUG mode, log to the console as well.
156
            console_log = logging.StreamHandler()
157
            console_log.setLevel(log_level)
158
            console_log.setFormatter(logging.Formatter(format_str))
159
            logging.getLogger('').addHandler(console_log)
160
161
        logging.info("Logging to %s with a minimum level of %s",
162
                     log_path, log_level_name)
163
164
        _logging_setup = True
165
166
    def ensure_profile_logger(self):
167
        """
168
        Sets up the profiling logger, if it hasn't already been set up.
169
        """
170
        global _profile_log
171
172
        enabled = getattr(settings, 'LOGGING_ENABLED', False)
173
        log_directory = getattr(settings, 'LOGGING_DIRECTORY', None)
174
        log_name = getattr(settings, 'LOGGING_NAME', None)
175
176
        if (enabled and log_directory and log_name and not _profile_log and
177
            getattr(settings, "LOGGING_ALLOW_PROFILING", False)):
178
            handler = logging.FileHandler(
179
                os.path.join(log_directory, log_name + ".prof"))
180
            handler.setLevel(logging.INFO)
181
            handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
182
183
            _profile_log = logging.getLogger("profile")
184
            _profile_log.addHandler(handler)
/trunk/djblets/djblets/util/misc.py
Revision 11802 New Change
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
#
24
#
25
25
26
26
27
import logging
28
27
from django.core.cache import cache
29
from django.core.cache import cache
28
from django.conf import settings
30
from django.conf import settings
29
from django.conf.urls.defaults import url
31
from django.conf.urls.defaults import url
30
from django.contrib.sites.models import Site
32
from django.contrib.sites.models import Site
31
from django.db.models.manager import Manager
33
from django.db.models.manager import Manager
45
        pass
47
        pass
46
48
47
    if not force_overwrite and cache.has_key(key):
49
    if not force_overwrite and cache.has_key(key):
48
        return cache.get(key)
50
        return cache.get(key)
49
    data = lookup_callable()
51
    data = lookup_callable()
52
53
    # Most people will be using memcached, and memcached has a limit of 1MB.
54
    # Data this big should be broken up somehow, so let's warn about this.
55
    if len(data) >= 1024 * 1024:
56
        logging.warning("Cache data for key %s (length %s) may be too big "
57
                        "for the cache." % (key, len(data)))
58
50
    try:
59
    try:
51
        cache.set(key, data, expiration)
60
        cache.set(key, data, expiration)
52
    except:
61
    except:
53
        pass
62
        pass
54
    return data
63
    return data
  1. /trunk/djblets/djblets/logging/__init__.py: 1 change [ new content ]
  2. /trunk/djblets/djblets/logging/middleware.py: 1 change [ new content ]
  3. /trunk/djblets/djblets/util/misc.py: 2 changes [ 1 2 ]