We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Properly rotate logging
# Other bugs

<!-- 
Did you find a bug with something other than a command? Fill out the following:
-->

#### What were you trying to do?

Check log files due to no disk space. 12gb for 2 of reds files. The logs are only labelled as part 1 and 9 with nothing in between.

#### What were you expecting to happen?

Log files to correctly rotate and not be just labelled as 1 and 9

#### What actually happened?

<!-- Replace this line with a description of what actually happened. Include any error messages -->

#### How can we reproduce this issue?

Just running red with ~100 cogs.


![](https://i.flaree.xyz/WindowsTerminal_0UK1SRb72M.png)



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `redbot/logging.py`
Content:
```
1 import logging.handlers
2 import pathlib
3 import re
4 import sys
5 
6 from typing import List, Tuple, Optional, Union
7 from logging import LogRecord
8 from datetime import datetime  # This clearly never leads to confusion...
9 from os import isatty
10 
11 from rich._log_render import LogRender
12 from rich.containers import Renderables
13 from rich.logging import RichHandler
14 from rich.table import Table
15 from rich.text import Text
16 from rich.traceback import Traceback
17 
18 
19 MAX_OLD_LOGS = 8
20 
21 
22 class RotatingFileHandler(logging.handlers.RotatingFileHandler):
23     """Custom rotating file handler.
24 
25     This file handler rotates a bit differently to the one in stdlib.
26 
27     For a start, this works off of a "stem" and a "directory". The stem
28     is the base name of the log file, without the extension. The
29     directory is where all log files (including backups) will be placed.
30 
31     Secondly, this logger rotates files downwards, and new logs are
32     *started* with the backup number incremented. The stdlib handler
33     rotates files upwards, and this leaves the logs in reverse order.
34 
35     Thirdly, naming conventions are not customisable with this class.
36     Logs will initially be named in the format "{stem}.log", and after
37     rotating, the first log file will be renamed "{stem}-part1.log",
38     and a new file "{stem}-part2.log" will be created for logging to
39     continue.
40 
41     A few things can't be modified in this handler: it must use append
42     mode, it doesn't support use of the `delay` arg, and it will ignore
43     custom namers and rotators.
44 
45     When this handler is instantiated, it will search through the
46     directory for logs from previous runtimes, and will open the file
47     with the highest backup number to append to.
48     """
49 
50     def __init__(
51         self,
52         stem: str,
53         directory: pathlib.Path,
54         maxBytes: int = 0,
55         backupCount: int = 0,
56         encoding: Optional[str] = None,
57     ) -> None:
58         self.baseStem = stem
59         self.directory = directory.resolve()
60         # Scan for existing files in directory, append to last part of existing log
61         log_part_re = re.compile(rf"{stem}-part(?P<partnum>\d+).log")
62         highest_part = 0
63         for path in directory.iterdir():
64             match = log_part_re.match(path.name)
65             if match and int(match["partnum"]) > highest_part:
66                 highest_part = int(match["partnum"])
67         if highest_part:
68             filename = directory / f"{stem}-part{highest_part}.log"
69         else:
70             filename = directory / f"{stem}.log"
71         super().__init__(
72             filename,
73             mode="a",
74             maxBytes=maxBytes,
75             backupCount=backupCount,
76             encoding=encoding,
77             delay=False,
78         )
79 
80     def doRollover(self):
81         if self.stream:
82             self.stream.close()
83             self.stream = None
84         initial_path = self.directory / f"{self.baseStem}.log"
85         if self.backupCount > 0 and initial_path.exists():
86             initial_path.replace(self.directory / f"{self.baseStem}-part1.log")
87 
88         match = re.match(
89             rf"{self.baseStem}(?:-part(?P<part>\d+)?)?.log", pathlib.Path(self.baseFilename).name
90         )
91         latest_part_num = int(match.groupdict(default="1").get("part", "1"))
92         if self.backupCount < 1:
93             # No backups, just delete the existing log and start again
94             pathlib.Path(self.baseFilename).unlink()
95         elif latest_part_num > self.backupCount:
96             # Rotate files down one
97             # red-part2.log becomes red-part1.log etc, a new log is added at the end.
98             for i in range(1, self.backupCount):
99                 next_log = self.directory / f"{self.baseStem}-part{i + 1}.log"
100                 if next_log.exists():
101                     prev_log = self.directory / f"{self.baseStem}-part{i}.log"
102                     next_log.replace(prev_log)
103         else:
104             # Simply start a new file
105             self.baseFilename = str(
106                 self.directory / f"{self.baseStem}-part{latest_part_num + 1}.log"
107             )
108 
109         self.stream = self._open()
110 
111 
112 class RedLogRender(LogRender):
113     def __call__(
114         self,
115         console,
116         renderables,
117         log_time=None,
118         time_format=None,
119         level="",
120         path=None,
121         line_no=None,
122         link_path=None,
123         logger_name=None,
124     ):
125         output = Table.grid(padding=(0, 1))
126         output.expand = True
127         if self.show_time:
128             output.add_column(style="log.time")
129         if self.show_level:
130             output.add_column(style="log.level", width=self.level_width)
131         output.add_column(ratio=1, style="log.message", overflow="fold")
132         if self.show_path and path:
133             output.add_column(style="log.path")
134         if logger_name:
135             output.add_column()
136         row = []
137         if self.show_time:
138             log_time = log_time or console.get_datetime()
139             log_time_display = log_time.strftime(time_format or self.time_format)
140             if log_time_display == self._last_time:
141                 row.append(Text(" " * len(log_time_display)))
142             else:
143                 row.append(Text(log_time_display))
144                 self._last_time = log_time_display
145         if self.show_level:
146             row.append(level)
147 
148         row.append(Renderables(renderables))
149         if self.show_path and path:
150             path_text = Text()
151             path_text.append(path, style=f"link file://{link_path}" if link_path else "")
152             if line_no:
153                 path_text.append(f":{line_no}")
154             row.append(path_text)
155 
156         if logger_name:
157             logger_name_text = Text()
158             logger_name_text.append(f"[{logger_name}]")
159             row.append(logger_name_text)
160 
161         output.add_row(*row)
162         return output
163 
164 
165 class RedRichHandler(RichHandler):
166     """Adaptation of Rich's RichHandler to manually adjust the path to a logger name"""
167 
168     def __init__(self, *args, **kwargs):
169         super().__init__(*args, **kwargs)
170         self._log_render = RedLogRender(
171             show_time=self._log_render.show_time,
172             show_level=self._log_render.show_level,
173             show_path=self._log_render.show_path,
174             level_width=self._log_render.level_width,
175         )
176 
177     def emit(self, record: LogRecord) -> None:
178         """Invoked by logging."""
179         path = pathlib.Path(record.pathname).name
180         level = self.get_level_text(record)
181         message = self.format(record)
182         time_format = None if self.formatter is None else self.formatter.datefmt
183         log_time = datetime.fromtimestamp(record.created)
184 
185         traceback = None
186         if self.rich_tracebacks and record.exc_info and record.exc_info != (None, None, None):
187             exc_type, exc_value, exc_traceback = record.exc_info
188             assert exc_type is not None
189             assert exc_value is not None
190             traceback = Traceback.from_exception(
191                 exc_type,
192                 exc_value,
193                 exc_traceback,
194                 width=self.tracebacks_width,
195                 extra_lines=self.tracebacks_extra_lines,
196                 theme=self.tracebacks_theme,
197                 word_wrap=self.tracebacks_word_wrap,
198                 show_locals=self.tracebacks_show_locals,
199                 locals_max_length=self.locals_max_length,
200                 locals_max_string=self.locals_max_string,
201             )
202             message = record.getMessage()
203 
204         use_markup = getattr(record, "markup") if hasattr(record, "markup") else self.markup
205         if use_markup:
206             message_text = Text.from_markup(message)
207         else:
208             message_text = Text(message)
209 
210         if self.highlighter:
211             message_text = self.highlighter(message_text)
212         if self.KEYWORDS:
213             message_text.highlight_words(self.KEYWORDS, "logging.keyword")
214 
215         self.console.print(
216             self._log_render(
217                 self.console,
218                 [message_text] if not traceback else [message_text, traceback],
219                 log_time=log_time,
220                 time_format=time_format,
221                 level=level,
222                 path=path,
223                 line_no=record.lineno,
224                 link_path=record.pathname if self.enable_link_path else None,
225                 logger_name=record.name,
226             )
227         )
228 
229 
230 def init_logging(
231     level: int, location: pathlib.Path, force_rich_logging: Union[bool, None]
232 ) -> None:
233     root_logger = logging.getLogger()
234 
235     base_logger = logging.getLogger("red")
236     base_logger.setLevel(level)
237     dpy_logger = logging.getLogger("discord")
238     dpy_logger.setLevel(logging.WARNING)
239     warnings_logger = logging.getLogger("py.warnings")
240     warnings_logger.setLevel(logging.WARNING)
241 
242     enable_rich_logging = False
243 
244     if isatty(0) and force_rich_logging is None:
245         # Check if the bot thinks it has a active terminal.
246         enable_rich_logging = True
247     elif force_rich_logging is True:
248         enable_rich_logging = True
249 
250     file_formatter = logging.Formatter(
251         "[{asctime}] [{levelname}] {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{"
252     )
253     if enable_rich_logging is True:
254         rich_formatter = logging.Formatter("{message}", datefmt="[%X]", style="{")
255 
256         stdout_handler = RedRichHandler(rich_tracebacks=True, show_path=False)
257         stdout_handler.setFormatter(rich_formatter)
258     else:
259         stdout_handler = logging.StreamHandler(sys.stdout)
260         stdout_handler.setFormatter(file_formatter)
261 
262     root_logger.addHandler(stdout_handler)
263     logging.captureWarnings(True)
264 
265     if not location.exists():
266         location.mkdir(parents=True, exist_ok=True)
267     # Rotate latest logs to previous logs
268     previous_logs: List[pathlib.Path] = []
269     latest_logs: List[Tuple[pathlib.Path, str]] = []
270     for path in location.iterdir():
271         match = re.match(r"latest(?P<part>-part\d+)?\.log", path.name)
272         if match:
273             part = match.groupdict(default="")["part"]
274             latest_logs.append((path, part))
275         match = re.match(r"previous(?:-part\d+)?.log", path.name)
276         if match:
277             previous_logs.append(path)
278     # Delete all previous.log files
279     for path in previous_logs:
280         path.unlink()
281     # Rename latest.log files to previous.log
282     for path, part in latest_logs:
283         path.replace(location / f"previous{part}.log")
284 
285     latest_fhandler = RotatingFileHandler(
286         stem="latest",
287         directory=location,
288         maxBytes=1_000_000,  # About 1MB per logfile
289         backupCount=MAX_OLD_LOGS,
290         encoding="utf-8",
291     )
292     all_fhandler = RotatingFileHandler(
293         stem="red",
294         directory=location,
295         maxBytes=1_000_000,
296         backupCount=MAX_OLD_LOGS,
297         encoding="utf-8",
298     )
299 
300     for fhandler in (latest_fhandler, all_fhandler):
301         fhandler.setFormatter(file_formatter)
302         root_logger.addHandler(fhandler)
303
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/redbot/logging.py b/redbot/logging.py
--- a/redbot/logging.py
+++ b/redbot/logging.py
@@ -58,7 +58,7 @@
         self.baseStem = stem
         self.directory = directory.resolve()
         # Scan for existing files in directory, append to last part of existing log
-        log_part_re = re.compile(rf"{stem}-part(?P<partnum>\d+).log")
+        log_part_re = re.compile(rf"{stem}-part(?P<partnum>\d)\.log")
         highest_part = 0
         for path in directory.iterdir():
             match = log_part_re.match(path.name)
@@ -86,7 +86,7 @@
             initial_path.replace(self.directory / f"{self.baseStem}-part1.log")
 
         match = re.match(
-            rf"{self.baseStem}(?:-part(?P<part>\d+)?)?.log", pathlib.Path(self.baseFilename).name
+            rf"{self.baseStem}(?:-part(?P<part>\d))?\.log", pathlib.Path(self.baseFilename).name
         )
         latest_part_num = int(match.groupdict(default="1").get("part", "1"))
         if self.backupCount < 1:
@@ -95,7 +95,7 @@
         elif latest_part_num > self.backupCount:
             # Rotate files down one
             # red-part2.log becomes red-part1.log etc, a new log is added at the end.
-            for i in range(1, self.backupCount):
+            for i in range(1, self.backupCount + 1):
                 next_log = self.directory / f"{self.baseStem}-part{i + 1}.log"
                 if next_log.exists():
                     prev_log = self.directory / f"{self.baseStem}-part{i}.log"

{"golden_diff": "diff --git a/redbot/logging.py b/redbot/logging.py\n--- a/redbot/logging.py\n+++ b/redbot/logging.py\n@@ -58,7 +58,7 @@\n         self.baseStem = stem\n         self.directory = directory.resolve()\n         # Scan for existing files in directory, append to last part of existing log\n-        log_part_re = re.compile(rf\"{stem}-part(?P<partnum>\\d+).log\")\n+        log_part_re = re.compile(rf\"{stem}-part(?P<partnum>\\d)\\.log\")\n         highest_part = 0\n         for path in directory.iterdir():\n             match = log_part_re.match(path.name)\n@@ -86,7 +86,7 @@\n             initial_path.replace(self.directory / f\"{self.baseStem}-part1.log\")\n \n         match = re.match(\n-            rf\"{self.baseStem}(?:-part(?P<part>\\d+)?)?.log\", pathlib.Path(self.baseFilename).name\n+            rf\"{self.baseStem}(?:-part(?P<part>\\d))?\\.log\", pathlib.Path(self.baseFilename).name\n         )\n         latest_part_num = int(match.groupdict(default=\"1\").get(\"part\", \"1\"))\n         if self.backupCount < 1:\n@@ -95,7 +95,7 @@\n         elif latest_part_num > self.backupCount:\n             # Rotate files down one\n             # red-part2.log becomes red-part1.log etc, a new log is added at the end.\n-            for i in range(1, self.backupCount):\n+            for i in range(1, self.backupCount + 1):\n                 next_log = self.directory / f\"{self.baseStem}-part{i + 1}.log\"\n                 if next_log.exists():\n                     prev_log = self.directory / f\"{self.baseStem}-part{i}.log\"\n", "issue": "Properly rotate logging\n# Other bugs\r\n\r\n<!-- \r\nDid you find a bug with something other than a command? Fill out the following:\r\n-->\r\n\r\n#### What were you trying to do?\r\n\r\nCheck log files due to no disk space. 12gb for 2 of reds files. The logs are only labelled as part 1 and 9 with nothing in between.\r\n\r\n#### What were you expecting to happen?\r\n\r\nLog files to correctly rotate and not be just labelled as 1 and 9\r\n\r\n#### What actually happened?\r\n\r\n<!-- Replace this line with a description of what actually happened. Include any error messages -->\r\n\r\n#### How can we reproduce this issue?\r\n\r\nJust running red with ~100 cogs.\r\n\r\n\r\n![](https://i.flaree.xyz/WindowsTerminal_0UK1SRb72M.png)\r\n\r\n\n", "before_files": [{"content": "import logging.handlers\nimport pathlib\nimport re\nimport sys\n\nfrom typing import List, Tuple, Optional, Union\nfrom logging import LogRecord\nfrom datetime import datetime  # This clearly never leads to confusion...\nfrom os import isatty\n\nfrom rich._log_render import LogRender\nfrom rich.containers import Renderables\nfrom rich.logging import RichHandler\nfrom rich.table import Table\nfrom rich.text import Text\nfrom rich.traceback import Traceback\n\n\nMAX_OLD_LOGS = 8\n\n\nclass RotatingFileHandler(logging.handlers.RotatingFileHandler):\n    \"\"\"Custom rotating file handler.\n\n    This file handler rotates a bit differently to the one in stdlib.\n\n    For a start, this works off of a \"stem\" and a \"directory\". The stem\n    is the base name of the log file, without the extension. The\n    directory is where all log files (including backups) will be placed.\n\n    Secondly, this logger rotates files downwards, and new logs are\n    *started* with the backup number incremented. The stdlib handler\n    rotates files upwards, and this leaves the logs in reverse order.\n\n    Thirdly, naming conventions are not customisable with this class.\n    Logs will initially be named in the format \"{stem}.log\", and after\n    rotating, the first log file will be renamed \"{stem}-part1.log\",\n    and a new file \"{stem}-part2.log\" will be created for logging to\n    continue.\n\n    A few things can't be modified in this handler: it must use append\n    mode, it doesn't support use of the `delay` arg, and it will ignore\n    custom namers and rotators.\n\n    When this handler is instantiated, it will search through the\n    directory for logs from previous runtimes, and will open the file\n    with the highest backup number to append to.\n    \"\"\"\n\n    def __init__(\n        self,\n        stem: str,\n        directory: pathlib.Path,\n        maxBytes: int = 0,\n        backupCount: int = 0,\n        encoding: Optional[str] = None,\n    ) -> None:\n        self.baseStem = stem\n        self.directory = directory.resolve()\n        # Scan for existing files in directory, append to last part of existing log\n        log_part_re = re.compile(rf\"{stem}-part(?P<partnum>\\d+).log\")\n        highest_part = 0\n        for path in directory.iterdir():\n            match = log_part_re.match(path.name)\n            if match and int(match[\"partnum\"]) > highest_part:\n                highest_part = int(match[\"partnum\"])\n        if highest_part:\n            filename = directory / f\"{stem}-part{highest_part}.log\"\n        else:\n            filename = directory / f\"{stem}.log\"\n        super().__init__(\n            filename,\n            mode=\"a\",\n            maxBytes=maxBytes,\n            backupCount=backupCount,\n            encoding=encoding,\n            delay=False,\n        )\n\n    def doRollover(self):\n        if self.stream:\n            self.stream.close()\n            self.stream = None\n        initial_path = self.directory / f\"{self.baseStem}.log\"\n        if self.backupCount > 0 and initial_path.exists():\n            initial_path.replace(self.directory / f\"{self.baseStem}-part1.log\")\n\n        match = re.match(\n            rf\"{self.baseStem}(?:-part(?P<part>\\d+)?)?.log\", pathlib.Path(self.baseFilename).name\n        )\n        latest_part_num = int(match.groupdict(default=\"1\").get(\"part\", \"1\"))\n        if self.backupCount < 1:\n            # No backups, just delete the existing log and start again\n            pathlib.Path(self.baseFilename).unlink()\n        elif latest_part_num > self.backupCount:\n            # Rotate files down one\n            # red-part2.log becomes red-part1.log etc, a new log is added at the end.\n            for i in range(1, self.backupCount):\n                next_log = self.directory / f\"{self.baseStem}-part{i + 1}.log\"\n                if next_log.exists():\n                    prev_log = self.directory / f\"{self.baseStem}-part{i}.log\"\n                    next_log.replace(prev_log)\n        else:\n            # Simply start a new file\n            self.baseFilename = str(\n                self.directory / f\"{self.baseStem}-part{latest_part_num + 1}.log\"\n            )\n\n        self.stream = self._open()\n\n\nclass RedLogRender(LogRender):\n    def __call__(\n        self,\n        console,\n        renderables,\n        log_time=None,\n        time_format=None,\n        level=\"\",\n        path=None,\n        line_no=None,\n        link_path=None,\n        logger_name=None,\n    ):\n        output = Table.grid(padding=(0, 1))\n        output.expand = True\n        if self.show_time:\n            output.add_column(style=\"log.time\")\n        if self.show_level:\n            output.add_column(style=\"log.level\", width=self.level_width)\n        output.add_column(ratio=1, style=\"log.message\", overflow=\"fold\")\n        if self.show_path and path:\n            output.add_column(style=\"log.path\")\n        if logger_name:\n            output.add_column()\n        row = []\n        if self.show_time:\n            log_time = log_time or console.get_datetime()\n            log_time_display = log_time.strftime(time_format or self.time_format)\n            if log_time_display == self._last_time:\n                row.append(Text(\" \" * len(log_time_display)))\n            else:\n                row.append(Text(log_time_display))\n                self._last_time = log_time_display\n        if self.show_level:\n            row.append(level)\n\n        row.append(Renderables(renderables))\n        if self.show_path and path:\n            path_text = Text()\n            path_text.append(path, style=f\"link file://{link_path}\" if link_path else \"\")\n            if line_no:\n                path_text.append(f\":{line_no}\")\n            row.append(path_text)\n\n        if logger_name:\n            logger_name_text = Text()\n            logger_name_text.append(f\"[{logger_name}]\")\n            row.append(logger_name_text)\n\n        output.add_row(*row)\n        return output\n\n\nclass RedRichHandler(RichHandler):\n    \"\"\"Adaptation of Rich's RichHandler to manually adjust the path to a logger name\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._log_render = RedLogRender(\n            show_time=self._log_render.show_time,\n            show_level=self._log_render.show_level,\n            show_path=self._log_render.show_path,\n            level_width=self._log_render.level_width,\n        )\n\n    def emit(self, record: LogRecord) -> None:\n        \"\"\"Invoked by logging.\"\"\"\n        path = pathlib.Path(record.pathname).name\n        level = self.get_level_text(record)\n        message = self.format(record)\n        time_format = None if self.formatter is None else self.formatter.datefmt\n        log_time = datetime.fromtimestamp(record.created)\n\n        traceback = None\n        if self.rich_tracebacks and record.exc_info and record.exc_info != (None, None, None):\n            exc_type, exc_value, exc_traceback = record.exc_info\n            assert exc_type is not None\n            assert exc_value is not None\n            traceback = Traceback.from_exception(\n                exc_type,\n                exc_value,\n                exc_traceback,\n                width=self.tracebacks_width,\n                extra_lines=self.tracebacks_extra_lines,\n                theme=self.tracebacks_theme,\n                word_wrap=self.tracebacks_word_wrap,\n                show_locals=self.tracebacks_show_locals,\n                locals_max_length=self.locals_max_length,\n                locals_max_string=self.locals_max_string,\n            )\n            message = record.getMessage()\n\n        use_markup = getattr(record, \"markup\") if hasattr(record, \"markup\") else self.markup\n        if use_markup:\n            message_text = Text.from_markup(message)\n        else:\n            message_text = Text(message)\n\n        if self.highlighter:\n            message_text = self.highlighter(message_text)\n        if self.KEYWORDS:\n            message_text.highlight_words(self.KEYWORDS, \"logging.keyword\")\n\n        self.console.print(\n            self._log_render(\n                self.console,\n                [message_text] if not traceback else [message_text, traceback],\n                log_time=log_time,\n                time_format=time_format,\n                level=level,\n                path=path,\n                line_no=record.lineno,\n                link_path=record.pathname if self.enable_link_path else None,\n                logger_name=record.name,\n            )\n        )\n\n\ndef init_logging(\n    level: int, location: pathlib.Path, force_rich_logging: Union[bool, None]\n) -> None:\n    root_logger = logging.getLogger()\n\n    base_logger = logging.getLogger(\"red\")\n    base_logger.setLevel(level)\n    dpy_logger = logging.getLogger(\"discord\")\n    dpy_logger.setLevel(logging.WARNING)\n    warnings_logger = logging.getLogger(\"py.warnings\")\n    warnings_logger.setLevel(logging.WARNING)\n\n    enable_rich_logging = False\n\n    if isatty(0) and force_rich_logging is None:\n        # Check if the bot thinks it has a active terminal.\n        enable_rich_logging = True\n    elif force_rich_logging is True:\n        enable_rich_logging = True\n\n    file_formatter = logging.Formatter(\n        \"[{asctime}] [{levelname}] {name}: {message}\", datefmt=\"%Y-%m-%d %H:%M:%S\", style=\"{\"\n    )\n    if enable_rich_logging is True:\n        rich_formatter = logging.Formatter(\"{message}\", datefmt=\"[%X]\", style=\"{\")\n\n        stdout_handler = RedRichHandler(rich_tracebacks=True, show_path=False)\n        stdout_handler.setFormatter(rich_formatter)\n    else:\n        stdout_handler = logging.StreamHandler(sys.stdout)\n        stdout_handler.setFormatter(file_formatter)\n\n    root_logger.addHandler(stdout_handler)\n    logging.captureWarnings(True)\n\n    if not location.exists():\n        location.mkdir(parents=True, exist_ok=True)\n    # Rotate latest logs to previous logs\n    previous_logs: List[pathlib.Path] = []\n    latest_logs: List[Tuple[pathlib.Path, str]] = []\n    for path in location.iterdir():\n        match = re.match(r\"latest(?P<part>-part\\d+)?\\.log\", path.name)\n        if match:\n            part = match.groupdict(default=\"\")[\"part\"]\n            latest_logs.append((path, part))\n        match = re.match(r\"previous(?:-part\\d+)?.log\", path.name)\n        if match:\n            previous_logs.append(path)\n    # Delete all previous.log files\n    for path in previous_logs:\n        path.unlink()\n    # Rename latest.log files to previous.log\n    for path, part in latest_logs:\n        path.replace(location / f\"previous{part}.log\")\n\n    latest_fhandler = RotatingFileHandler(\n        stem=\"latest\",\n        directory=location,\n        maxBytes=1_000_000,  # About 1MB per logfile\n        backupCount=MAX_OLD_LOGS,\n        encoding=\"utf-8\",\n    )\n    all_fhandler = RotatingFileHandler(\n        stem=\"red\",\n        directory=location,\n        maxBytes=1_000_000,\n        backupCount=MAX_OLD_LOGS,\n        encoding=\"utf-8\",\n    )\n\n    for fhandler in (latest_fhandler, all_fhandler):\n        fhandler.setFormatter(file_formatter)\n        root_logger.addHandler(fhandler)\n", "path": "redbot/logging.py"}], "after_files": [{"content": "import logging.handlers\nimport pathlib\nimport re\nimport sys\n\nfrom typing import List, Tuple, Optional, Union\nfrom logging import LogRecord\nfrom datetime import datetime  # This clearly never leads to confusion...\nfrom os import isatty\n\nfrom rich._log_render import LogRender\nfrom rich.containers import Renderables\nfrom rich.logging import RichHandler\nfrom rich.table import Table\nfrom rich.text import Text\nfrom rich.traceback import Traceback\n\n\nMAX_OLD_LOGS = 8\n\n\nclass RotatingFileHandler(logging.handlers.RotatingFileHandler):\n    \"\"\"Custom rotating file handler.\n\n    This file handler rotates a bit differently to the one in stdlib.\n\n    For a start, this works off of a \"stem\" and a \"directory\". The stem\n    is the base name of the log file, without the extension. The\n    directory is where all log files (including backups) will be placed.\n\n    Secondly, this logger rotates files downwards, and new logs are\n    *started* with the backup number incremented. The stdlib handler\n    rotates files upwards, and this leaves the logs in reverse order.\n\n    Thirdly, naming conventions are not customisable with this class.\n    Logs will initially be named in the format \"{stem}.log\", and after\n    rotating, the first log file will be renamed \"{stem}-part1.log\",\n    and a new file \"{stem}-part2.log\" will be created for logging to\n    continue.\n\n    A few things can't be modified in this handler: it must use append\n    mode, it doesn't support use of the `delay` arg, and it will ignore\n    custom namers and rotators.\n\n    When this handler is instantiated, it will search through the\n    directory for logs from previous runtimes, and will open the file\n    with the highest backup number to append to.\n    \"\"\"\n\n    def __init__(\n        self,\n        stem: str,\n        directory: pathlib.Path,\n        maxBytes: int = 0,\n        backupCount: int = 0,\n        encoding: Optional[str] = None,\n    ) -> None:\n        self.baseStem = stem\n        self.directory = directory.resolve()\n        # Scan for existing files in directory, append to last part of existing log\n        log_part_re = re.compile(rf\"{stem}-part(?P<partnum>\\d)\\.log\")\n        highest_part = 0\n        for path in directory.iterdir():\n            match = log_part_re.match(path.name)\n            if match and int(match[\"partnum\"]) > highest_part:\n                highest_part = int(match[\"partnum\"])\n        if highest_part:\n            filename = directory / f\"{stem}-part{highest_part}.log\"\n        else:\n            filename = directory / f\"{stem}.log\"\n        super().__init__(\n            filename,\n            mode=\"a\",\n            maxBytes=maxBytes,\n            backupCount=backupCount,\n            encoding=encoding,\n            delay=False,\n        )\n\n    def doRollover(self):\n        if self.stream:\n            self.stream.close()\n            self.stream = None\n        initial_path = self.directory / f\"{self.baseStem}.log\"\n        if self.backupCount > 0 and initial_path.exists():\n            initial_path.replace(self.directory / f\"{self.baseStem}-part1.log\")\n\n        match = re.match(\n            rf\"{self.baseStem}(?:-part(?P<part>\\d))?\\.log\", pathlib.Path(self.baseFilename).name\n        )\n        latest_part_num = int(match.groupdict(default=\"1\").get(\"part\", \"1\"))\n        if self.backupCount < 1:\n            # No backups, just delete the existing log and start again\n            pathlib.Path(self.baseFilename).unlink()\n        elif latest_part_num > self.backupCount:\n            # Rotate files down one\n            # red-part2.log becomes red-part1.log etc, a new log is added at the end.\n            for i in range(1, self.backupCount + 1):\n                next_log = self.directory / f\"{self.baseStem}-part{i + 1}.log\"\n                if next_log.exists():\n                    prev_log = self.directory / f\"{self.baseStem}-part{i}.log\"\n                    next_log.replace(prev_log)\n        else:\n            # Simply start a new file\n            self.baseFilename = str(\n                self.directory / f\"{self.baseStem}-part{latest_part_num + 1}.log\"\n            )\n\n        self.stream = self._open()\n\n\nclass RedLogRender(LogRender):\n    def __call__(\n        self,\n        console,\n        renderables,\n        log_time=None,\n        time_format=None,\n        level=\"\",\n        path=None,\n        line_no=None,\n        link_path=None,\n        logger_name=None,\n    ):\n        output = Table.grid(padding=(0, 1))\n        output.expand = True\n        if self.show_time:\n            output.add_column(style=\"log.time\")\n        if self.show_level:\n            output.add_column(style=\"log.level\", width=self.level_width)\n        output.add_column(ratio=1, style=\"log.message\", overflow=\"fold\")\n        if self.show_path and path:\n            output.add_column(style=\"log.path\")\n        if logger_name:\n            output.add_column()\n        row = []\n        if self.show_time:\n            log_time = log_time or console.get_datetime()\n            log_time_display = log_time.strftime(time_format or self.time_format)\n            if log_time_display == self._last_time:\n                row.append(Text(\" \" * len(log_time_display)))\n            else:\n                row.append(Text(log_time_display))\n                self._last_time = log_time_display\n        if self.show_level:\n            row.append(level)\n\n        row.append(Renderables(renderables))\n        if self.show_path and path:\n            path_text = Text()\n            path_text.append(path, style=f\"link file://{link_path}\" if link_path else \"\")\n            if line_no:\n                path_text.append(f\":{line_no}\")\n            row.append(path_text)\n\n        if logger_name:\n            logger_name_text = Text()\n            logger_name_text.append(f\"[{logger_name}]\")\n            row.append(logger_name_text)\n\n        output.add_row(*row)\n        return output\n\n\nclass RedRichHandler(RichHandler):\n    \"\"\"Adaptation of Rich's RichHandler to manually adjust the path to a logger name\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._log_render = RedLogRender(\n            show_time=self._log_render.show_time,\n            show_level=self._log_render.show_level,\n            show_path=self._log_render.show_path,\n            level_width=self._log_render.level_width,\n        )\n\n    def emit(self, record: LogRecord) -> None:\n        \"\"\"Invoked by logging.\"\"\"\n        path = pathlib.Path(record.pathname).name\n        level = self.get_level_text(record)\n        message = self.format(record)\n        time_format = None if self.formatter is None else self.formatter.datefmt\n        log_time = datetime.fromtimestamp(record.created)\n\n        traceback = None\n        if self.rich_tracebacks and record.exc_info and record.exc_info != (None, None, None):\n            exc_type, exc_value, exc_traceback = record.exc_info\n            assert exc_type is not None\n            assert exc_value is not None\n            traceback = Traceback.from_exception(\n                exc_type,\n                exc_value,\n                exc_traceback,\n                width=self.tracebacks_width,\n                extra_lines=self.tracebacks_extra_lines,\n                theme=self.tracebacks_theme,\n                word_wrap=self.tracebacks_word_wrap,\n                show_locals=self.tracebacks_show_locals,\n                locals_max_length=self.locals_max_length,\n                locals_max_string=self.locals_max_string,\n            )\n            message = record.getMessage()\n\n        use_markup = getattr(record, \"markup\") if hasattr(record, \"markup\") else self.markup\n        if use_markup:\n            message_text = Text.from_markup(message)\n        else:\n            message_text = Text(message)\n\n        if self.highlighter:\n            message_text = self.highlighter(message_text)\n        if self.KEYWORDS:\n            message_text.highlight_words(self.KEYWORDS, \"logging.keyword\")\n\n        self.console.print(\n            self._log_render(\n                self.console,\n                [message_text] if not traceback else [message_text, traceback],\n                log_time=log_time,\n                time_format=time_format,\n                level=level,\n                path=path,\n                line_no=record.lineno,\n                link_path=record.pathname if self.enable_link_path else None,\n                logger_name=record.name,\n            )\n        )\n\n\ndef init_logging(\n    level: int, location: pathlib.Path, force_rich_logging: Union[bool, None]\n) -> None:\n    root_logger = logging.getLogger()\n\n    base_logger = logging.getLogger(\"red\")\n    base_logger.setLevel(level)\n    dpy_logger = logging.getLogger(\"discord\")\n    dpy_logger.setLevel(logging.WARNING)\n    warnings_logger = logging.getLogger(\"py.warnings\")\n    warnings_logger.setLevel(logging.WARNING)\n\n    enable_rich_logging = False\n\n    if isatty(0) and force_rich_logging is None:\n        # Check if the bot thinks it has a active terminal.\n        enable_rich_logging = True\n    elif force_rich_logging is True:\n        enable_rich_logging = True\n\n    file_formatter = logging.Formatter(\n        \"[{asctime}] [{levelname}] {name}: {message}\", datefmt=\"%Y-%m-%d %H:%M:%S\", style=\"{\"\n    )\n    if enable_rich_logging is True:\n        rich_formatter = logging.Formatter(\"{message}\", datefmt=\"[%X]\", style=\"{\")\n\n        stdout_handler = RedRichHandler(rich_tracebacks=True, show_path=False)\n        stdout_handler.setFormatter(rich_formatter)\n    else:\n        stdout_handler = logging.StreamHandler(sys.stdout)\n        stdout_handler.setFormatter(file_formatter)\n\n    root_logger.addHandler(stdout_handler)\n    logging.captureWarnings(True)\n\n    if not location.exists():\n        location.mkdir(parents=True, exist_ok=True)\n    # Rotate latest logs to previous logs\n    previous_logs: List[pathlib.Path] = []\n    latest_logs: List[Tuple[pathlib.Path, str]] = []\n    for path in location.iterdir():\n        match = re.match(r\"latest(?P<part>-part\\d+)?\\.log\", path.name)\n        if match:\n            part = match.groupdict(default=\"\")[\"part\"]\n            latest_logs.append((path, part))\n        match = re.match(r\"previous(?:-part\\d+)?.log\", path.name)\n        if match:\n            previous_logs.append(path)\n    # Delete all previous.log files\n    for path in previous_logs:\n        path.unlink()\n    # Rename latest.log files to previous.log\n    for path, part in latest_logs:\n        path.replace(location / f\"previous{part}.log\")\n\n    latest_fhandler = RotatingFileHandler(\n        stem=\"latest\",\n        directory=location,\n        maxBytes=1_000_000,  # About 1MB per logfile\n        backupCount=MAX_OLD_LOGS,\n        encoding=\"utf-8\",\n    )\n    all_fhandler = RotatingFileHandler(\n        stem=\"red\",\n        directory=location,\n        maxBytes=1_000_000,\n        backupCount=MAX_OLD_LOGS,\n        encoding=\"utf-8\",\n    )\n\n    for fhandler in (latest_fhandler, all_fhandler):\n        fhandler.setFormatter(file_formatter)\n        root_logger.addHandler(fhandler)\n", "path": "redbot/logging.py"}]}