Support rotating devserver logs.
If the devserver is started with a log file specified, the log
handler will be configured to rotate the logs once a week, to
prevent the logs from growing unmanageably large.
BUG=chromium:219168
TEST=test the feature with log rotation temporarily set to one minute
Change-Id: I0252b928c470b9d053efd4e3148eba8857bbb19e
Reviewed-on: https://gerrit.chromium.org/gerrit/48931
Commit-Queue: Richard Barnette <jrbarnette@chromium.org>
Reviewed-by: Richard Barnette <jrbarnette@chromium.org>
Tested-by: Richard Barnette <jrbarnette@chromium.org>
diff --git a/devserver.py b/devserver.py
index ec74816..11e8937 100755
--- a/devserver.py
+++ b/devserver.py
@@ -41,17 +41,20 @@
"""
-import cherrypy
import json
import optparse
import os
import re
import shutil
import socket
-import sys
import subprocess
+import sys
import tempfile
import types
+from logging import handlers
+
+import cherrypy
+import cherrypy._cplogging
import autoupdate
import common_util
@@ -75,6 +78,14 @@
# Sets up global to share between classes.
updater = None
+# Log rotation parameters. These settings correspond to once a week
+# on Saturday, with about three months of old logs kept for backup.
+#
+# For more, see the documentation for
+# logging.handlers.TimedRotatingFileHandler
+_LOG_ROTATION_TIME = 'W5'
+_LOG_ROTATION_BACKUP = 13
+
class DevServerError(Exception):
"""Exception class used by this module."""
@@ -848,6 +859,18 @@
parser.add_option_group(group)
+def _MakeLogHandler(logfile):
+ """Create a LogHandler instance used to log all messages."""
+ hdlr_cls = handlers.TimedRotatingFileHandler
+ hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
+ backupCount=_LOG_ROTATION_BACKUP)
+ # The cherrypy documentation says to use the _cplogging module for
+ # this, even though it's named as a private module.
+ # pylint: disable=W0212
+ hdlr.setFormatter(cherrypy._cplogging.logfmt)
+ return hdlr
+
+
def main():
usage = '\n\n'.join(['usage: %prog [options]', __doc__])
parser = optparse.OptionParser(usage=usage)
@@ -868,6 +891,23 @@
_AddTestingOptions(parser)
(options, _) = parser.parse_args()
+ # Handle options that must be set globally in cherrypy. Do this
+ # work up front, because calls to _Log() below depend on this
+ # initialization.
+ if options.production:
+ cherrypy.config.update({'environment': 'production'})
+ if not options.logfile:
+ cherrypy.config.update({'log.screen': True})
+ else:
+ cherrypy.config.update({'log.error_file': '',
+ 'log.access_file': ''})
+ hdlr = _MakeLogHandler(options.logfile)
+ # Pylint can't seem to process these two calls properly
+ # pylint: disable=E1101
+ cherrypy.log.access_log.addHandler(hdlr)
+ cherrypy.log.error_log.addHandler(hdlr)
+ # pylint: enable=E1101
+
devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
root_dir = os.path.realpath('%s/../..' % devserver_dir)
serve_only = False
@@ -875,18 +915,18 @@
static_dir = os.path.realpath('%s/static' % options.data_dir)
os.system('mkdir -p %s' % static_dir)
- # TODO(sosa): Remove after depcrecation.
+ # TODO(sosa): Remove after deprecation.
if options.vm:
options.patch_kernel = False
if options.archive_dir:
- # TODO(zbehan) Remove legacy support:
- # archive_dir is the directory where static/archive will point.
- # If this is an absolute path, all is fine. If someone calls this
- # using a relative path, that is relative to src/platform/dev/.
- # That use case is unmaintainable, but since applications use it
- # with =./static, instead of a boolean flag, we'll make this relative
- # to devserver_dir to keep these unbroken. For now.
+ # TODO(zbehan) Remove legacy support:
+ # archive_dir is the directory where static/archive will point.
+ # If this is an absolute path, all is fine. If someone calls this
+ # using a relative path, that is relative to src/platform/dev/.
+ # That use case is unmaintainable, but since applications use it
+ # with =./static, instead of a boolean flag, we'll make this
+ # relative to devserver_dir to keep these unbroken. For now.
archive_dir = options.archive_dir
if not os.path.isabs(archive_dir):
archive_dir = os.path.realpath(os.path.join(devserver_dir, archive_dir))
@@ -895,15 +935,14 @@
serve_only = True
cache_dir = os.path.join(static_dir, 'cache')
- # If our devserver is only supposed to serve payloads, we shouldn't be mucking
- # with the cache at all. If the devserver hadn't previously generated a cache
- # and is expected, the caller is using it wrong.
+ # If our devserver is only supposed to serve payloads, we shouldn't be
+ # mucking with the cache at all. If the devserver hadn't previously
+ # generated a cache and is expected, the caller is using it wrong.
if serve_only:
# Extra check to make sure we're not being called incorrectly.
if (options.clear_cache or options.exit or options.pregenerate_update or
options.board or options.image):
parser.error('Incompatible flags detected for serve_only mode.')
-
elif os.path.exists(cache_dir):
_CleanCache(cache_dir, options.clear_cache)
else:
@@ -940,18 +979,10 @@
if options.pregenerate_update:
updater.PreGenerateUpdate()
- # If the command line requested after setup, it's time to do it.
- if not options.exit:
- # Handle options that must be set globally in cherrypy.
- if options.production:
- cherrypy.config.update({'environment': 'production'})
- if not options.logfile:
- cherrypy.config.update({'log.screen': True})
- else:
- cherrypy.config.update({'log.error_file': options.logfile,
- 'log.access_file': options.logfile})
+ if options.exit:
+ return
- cherrypy.quickstart(DevServerRoot(), config=_GetConfig(options))
+ cherrypy.quickstart(DevServerRoot(), config=_GetConfig(options))
if __name__ == '__main__':