[#1949] [UI] Allow setting max size for rotating log file

This commit is contained in:
bendikro 2016-01-15 22:08:44 +01:00
parent c90af1ce6c
commit 6300f9154a
5 changed files with 141 additions and 36 deletions

View File

@ -13,8 +13,10 @@ import base64
import gettext import gettext
import locale import locale
import logging import logging
import numbers
import os import os
import platform import platform
import re
import subprocess import subprocess
import sys import sys
import time import time
@ -465,6 +467,82 @@ def fdate(seconds, date_only=False, precision_secs=False):
return time.strftime("%x %H:%M", time.localtime(seconds)) return time.strftime("%x %H:%M", time.localtime(seconds))
def tokenize(text):
"""
Tokenize a text into numbers and strings.
Args:
text (str): The text to tokenize (a string).
Returns:
list: A list of strings and/or numbers.
This function is used to implement robust tokenization of user input
It automatically coerces integer and floating point numbers, ignores
whitespace and knows how to separate numbers from strings even without
whitespace.
"""
tokenized_input = []
for token in re.split(r'(\d+(?:\.\d+)?)', text):
token = token.strip()
if re.match(r'\d+\.\d+', token):
tokenized_input.append(float(token))
elif token.isdigit():
tokenized_input.append(int(token))
elif token:
tokenized_input.append(token)
return tokenized_input
size_units = (dict(prefix='b', divider=1, singular='byte', plural='bytes'),
dict(prefix='KiB', divider=1024**1),
dict(prefix='MiB', divider=1024**2),
dict(prefix='GiB', divider=1024**3),
dict(prefix='TiB', divider=1024**4),
dict(prefix='PiB', divider=1024**5),
dict(prefix='KB', divider=1000**1),
dict(prefix='MB', divider=1000**2),
dict(prefix='GB', divider=1000**3),
dict(prefix='TB', divider=1000**4),
dict(prefix='PB', divider=1000**5),
dict(prefix='m', divider=1000**2))
class InvalidSize(Exception):
pass
def parse_human_size(size):
"""
Parse a human readable data size and return the number of bytes.
Args:
size (str): The human readable file size to parse (a string).
Returns:
int: The corresponding size in bytes.
Raises:
InvalidSize: when the input can't be parsed.
"""
tokens = tokenize(size)
if tokens and isinstance(tokens[0], numbers.Number):
# If the input contains only a number, it's assumed to be the number of bytes.
if len(tokens) == 1:
return int(tokens[0])
# Otherwise we expect to find two tokens: A number and a unit.
if len(tokens) == 2 and isinstance(tokens[1], basestring):
normalized_unit = tokens[1].lower()
# Try to match the first letter of the unit.
for unit in size_units:
if normalized_unit.startswith(unit['prefix'].lower()):
return int(tokens[0] * unit['divider'])
# We failed to parse the size specification.
msg = "Failed to parse size! (input %r was tokenized as %r)"
raise InvalidSize(msg % (size, tokens))
def is_url(url): def is_url(url):
""" """
A simple test to check if the URL is valid A simple test to check if the URL is valid

View File

@ -107,15 +107,19 @@ levels = {
} }
def setup_logger(level="error", filename=None, filemode="w"): def setup_logger(level="error", filename=None, filemode="w", logrotate=None):
""" """
Sets up the basic logger and if `:param:filename` is set, then it will log Sets up the basic logger and if `:param:filename` is set, then it will log
to that file instead of stdout. to that file instead of stdout.
:param level: str, the level to log Args:
:param filename: str, the file to log to level (str): The log level to use (Default: "error")
filename (str, optional): The log filename. Default is None meaning log
to terminal
filemode (str): The filemode to use when opening the log file
logrotate (int, optional): The size of the logfile in bytes when enabling
log rotation (Default is None meaning disabled)
""" """
if logging.getLoggerClass() is not Logging: if logging.getLoggerClass() is not Logging:
logging.setLoggerClass(Logging) logging.setLoggerClass(Logging)
logging.addLevelName(5, "TRACE") logging.addLevelName(5, "TRACE")
@ -125,19 +129,16 @@ def setup_logger(level="error", filename=None, filemode="w"):
root_logger = logging.getLogger() root_logger = logging.getLogger()
if filename and filemode == "a": if filename and logrotate:
handler = logging.handlers.RotatingFileHandler( handler = logging.handlers.RotatingFileHandler(
filename, filemode, filename, maxBytes=logrotate,
maxBytes=50 * 1024 * 1024, # 50 Mb backupCount=5, encoding="utf-8"
backupCount=3,
encoding="utf-8",
delay=0
) )
elif filename and filemode == "w": elif filename and filemode == "w":
handler = getattr( handler = getattr(
logging.handlers, "WatchedFileHandler", logging.FileHandler)( logging.handlers, "WatchedFileHandler", logging.FileHandler)(
filename, filemode, "utf-8", delay=0 filename, mode=filemode, encoding="utf-8"
) )
else: else:
handler = logging.StreamHandler(stream=sys.stdout) handler = logging.StreamHandler(stream=sys.stdout)

View File

@ -74,3 +74,21 @@ class CommonTestCase(unittest.TestCase):
self.failUnless(VersionSplit("1.4.0") > VersionSplit("1.4.0.dev123")) self.failUnless(VersionSplit("1.4.0") > VersionSplit("1.4.0.dev123"))
self.failUnless(VersionSplit("1.4.0.dev1") < VersionSplit("1.4.0")) self.failUnless(VersionSplit("1.4.0.dev1") < VersionSplit("1.4.0"))
self.failUnless(VersionSplit("1.4.0a1") < VersionSplit("1.4.0")) self.failUnless(VersionSplit("1.4.0a1") < VersionSplit("1.4.0"))
def test_parse_human_size(self):
from deluge.common import parse_human_size
sizes = [("1", 1),
("10 bytes", 10),
("2048 bytes", 2048),
("1MiB", 2**(10 * 2)),
("1 MiB", 2**(10 * 2)),
("1 GiB", 2**(10 * 3)),
("1 GiB", 2**(10 * 3)),
("1M", 10**6),
("1MB", 10**6),
("1 GB", 10**9),
("1 TB", 10**12)]
for human_size, byte_size in sizes:
parsed = parse_human_size(human_size)
self.assertEquals(parsed, byte_size, "Mismatch when converting '%s'" % human_size)

View File

@ -14,21 +14,21 @@ import platform
import sys import sys
import textwrap import textwrap
import deluge.common
import deluge.configmanager import deluge.configmanager
import deluge.log import deluge.log
from deluge import common
from deluge.log import setup_logger from deluge.log import setup_logger
def get_version(): def get_version():
version_str = "%s\n" % (deluge.common.get_version()) version_str = "%s\n" % (common.get_version())
try: try:
from deluge._libtorrent import lt from deluge._libtorrent import lt
version_str += "libtorrent: %s\n" % lt.version version_str += "libtorrent: %s\n" % lt.version
except ImportError: except ImportError:
pass pass
version_str += "Python: %s\n" % platform.python_version() version_str += "Python: %s\n" % platform.python_version()
version_str += "OS: %s %s\n" % (platform.system(), " ".join(deluge.common.get_os_version())) version_str += "OS: %s %s\n" % (platform.system(), " ".join(common.get_os_version()))
return version_str return version_str
@ -93,6 +93,7 @@ class BaseArgParser(argparse.ArgumentParser):
kwargs["formatter_class"] = lambda prog: DelugeTextHelpFormatter(prog, max_help_position=33, width=90) kwargs["formatter_class"] = lambda prog: DelugeTextHelpFormatter(prog, max_help_position=33, width=90)
super(BaseArgParser, self).__init__(*args, add_help=False, **kwargs) super(BaseArgParser, self).__init__(*args, add_help=False, **kwargs)
self.common_setup = False
self.group = self.add_argument_group('Common Options') self.group = self.add_argument_group('Common Options')
self.group.add_argument('--version', action='version', version='%(prog)s ' + get_version(), self.group.add_argument('--version', action='version', version='%(prog)s ' + get_version(),
help="Show program's version number and exit") help="Show program's version number and exit")
@ -104,37 +105,43 @@ class BaseArgParser(argparse.ArgumentParser):
help="Set the log level: %s" % ", ".join(deluge.log.levels)) help="Set the log level: %s" % ", ".join(deluge.log.levels))
self.group.add_argument("-q", "--quiet", action="store_true", default=False, self.group.add_argument("-q", "--quiet", action="store_true", default=False,
help="Sets the log level to 'none', this is the same as `-L none`") help="Sets the log level to 'none', this is the same as `-L none`")
self.group.add_argument("-r", "--rotate-logs", action="store_true", default=False, self.group.add_argument("--log-rotate", action="store", nargs="?", const="50M",
help="Rotate logfiles.") help="Enable logfile rotation (optional max file size, default: %(const)s)."
"Log file rotate count is 5")
self.group.add_argument("--profile", metavar="<results file>", action="store", nargs="?", default=False, self.group.add_argument("--profile", metavar="<results file>", action="store", nargs="?", default=False,
help="Profile %(prog)s with cProfile. Prints results to stdout" help="Profile %(prog)s with cProfile. Prints results to stdout"
"unless a filename is specififed.") "unless a filename is specififed")
self.group.add_argument("-h", "--help", action=HelpAction, help='Show this help message and exit') self.group.add_argument("-h", "--help", action=HelpAction, help='Show this help message and exit')
def parse_args(self, *args): def parse_args(self, *args):
options, remaining = super(BaseArgParser, self).parse_known_args(*args) options, remaining = super(BaseArgParser, self).parse_known_args(*args)
options.remaining = remaining options.remaining = remaining
# Setup the logger if not self.common_setup:
if options.quiet: self.common_setup = True
options.loglevel = "none" # Setup the logger
if options.loglevel: if options.quiet:
options.loglevel = options.loglevel.lower() options.loglevel = "none"
if options.loglevel:
options.loglevel = options.loglevel.lower()
logfile_mode = 'w' logfile_mode = 'w'
if options.rotate_logs: logrotate = options.log_rotate
logfile_mode = 'a' if options.log_rotate:
logfile_mode = 'a'
logrotate = common.parse_human_size(options.log_rotate)
# Setup the logger # Setup the logger
setup_logger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode) setup_logger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode,
logrotate=logrotate)
if options.config: if options.config:
if not deluge.configmanager.set_config_dir(options.config): if not deluge.configmanager.set_config_dir(options.config):
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.error("There was an error setting the config dir! Exiting..") log.error("There was an error setting the config dir! Exiting..")
sys.exit(1) sys.exit(1)
else: else:
if not os.path.exists(deluge.common.get_default_config_dir()): if not os.path.exists(common.get_default_config_dir()):
os.makedirs(deluge.common.get_default_config_dir()) os.makedirs(common.get_default_config_dir())
return options return options

View File

@ -24,6 +24,7 @@ log = logging.getLogger(__name__)
# defined in setup.py # defined in setup.py
# #
def load_commands(command_dir): def load_commands(command_dir):
def get_command(name): def get_command(name):