Repository: evennia/evennia

Issue:
Feature request: auto-rotate of channel log files
#### Brief summary of issue / Description of requested feature:
Although log files like `portal.log` and `http_requests.log` will automatically rotate when over a 1MB threshold, the channel logs do not appear to do so, and seem to grow unbounded in size. It might be nice to have something that would automatically rotate log files when they pass over a certain length, while keeping the most recent lines in order to not empty the channel's `/history`.


#### Steps to reproduce the issue / Reasons for adding feature:

1. Rotation of channel logs to prevent them from growing too huge


#### Error output / Expected result of feature
Older log files becoming something like `public.log.1` while a new `public.log` is created that has the most recent messages once it passes some threshold defined in settings (or as an attribute in the channel, possibly?)


#### Extra information, such as Evennia revision/repo/branch, operating system and ideas for how to solve / implement:
I know that twisted has something to automatically rotate logs, but I'm unsure if it'd be best to use that or just something custom for evennia.



Discussion:
I see no reason not to - just need to figure out a sane and safe way to do it to an open file handle.

Resolving patch:
diff --git a/evennia/settings_default.py b/evennia/settings_default.py
--- a/evennia/settings_default.py
+++ b/evennia/settings_default.py
@@ -125,6 +125,10 @@
 # file sizes down. Turn off to get ever growing log files and never
 # loose log info.
 CYCLE_LOGFILES = True
+# Number of lines to append to rotating channel logs when they rotate
+CHANNEL_LOG_NUM_TAIL_LINES = 20
+# Max size of channel log files before they rotate
+CHANNEL_LOG_ROTATE_SIZE = 1000000
 # Local time zone for this installation. All choices can be found here:
 # http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
 TIME_ZONE = 'UTC'
diff --git a/evennia/utils/logger.py b/evennia/utils/logger.py
--- a/evennia/utils/logger.py
+++ b/evennia/utils/logger.py
@@ -19,7 +19,7 @@
 import time
 from datetime import datetime
 from traceback import format_exc
-from twisted.python import log
+from twisted.python import log, logfile
 from twisted.internet.threads import deferToThread
 
 
@@ -153,6 +153,53 @@ def log_dep(depmsg):
 
 # Arbitrary file logger
 
+class EvenniaLogFile(logfile.LogFile):
+    """
+    A rotating logfile based off Twisted's LogFile. It overrides
+    the LogFile's rotate method in order to append some of the last
+    lines of the previous log to the start of the new log, in order
+    to preserve a continuous chat history for channel log files.
+    """
+    from django.conf import settings
+    num_lines_to_append = settings.CHANNEL_LOG_NUM_TAIL_LINES
+
+    def rotate(self):
+        """
+        Rotates our log file and appends some number of lines from
+        the previous log to the start of the new one.
+        """
+        append_tail = self.num_lines_to_append > 0
+        if not append_tail:
+            logfile.LogFile.rotate(self)
+            return
+        lines = tail_log_file(self.path, 0, self.num_lines_to_append)
+        logfile.LogFile.rotate(self)
+        for line in lines:
+            self.write(line)
+
+    def seek(self, *args, **kwargs):
+        """
+        Convenience method for accessing our _file attribute's seek method,
+        which is used in tail_log_function.
+        Args:
+            *args: Same args as file.seek
+            **kwargs: Same kwargs as file.seek
+        """
+        return self._file.seek(*args, **kwargs)
+
+    def readlines(self, *args, **kwargs):
+        """
+        Convenience method for accessing our _file attribute's readlines method,
+        which is used in tail_log_function.
+        Args:
+            *args: same args as file.readlines
+            **kwargs: same kwargs as file.readlines
+
+        Returns:
+            lines (list): lines from our _file attribute.
+        """
+        return self._file.readlines(*args, **kwargs)
+
 _LOG_FILE_HANDLES = {}  # holds open log handles
 
 
@@ -162,9 +209,9 @@ def _open_log_file(filename):
     handle.  Will create a new file in the log dir if one didn't
     exist.
     """
+    from django.conf import settings
     global _LOG_FILE_HANDLES, _LOGDIR
     if not _LOGDIR:
-        from django.conf import settings
         _LOGDIR = settings.LOG_DIR
 
     filename = os.path.join(_LOGDIR, filename)
@@ -173,7 +220,8 @@ def _open_log_file(filename):
         return _LOG_FILE_HANDLES[filename]
     else:
         try:
-            filehandle = open(filename, "a+")  # append mode + reading
+            filehandle = EvenniaLogFile.fromFullPath(filename, rotateLength=settings.CHANNEL_LOG_ROTATE_SIZE)
+            # filehandle = open(filename, "a+")  # append mode + reading
             _LOG_FILE_HANDLES[filename] = filehandle
             return filehandle
         except IOError:


Test patch: