Skip to content

Commit f683fef

Browse files
authored
Merge pull request #56 from ilikecake/filehandler-roll-logs
Add a log rotation function to the file handler
2 parents 10a0db2 + c81d30e commit f683fef

File tree

1 file changed

+105
-1
lines changed

1 file changed

+105
-1
lines changed

adafruit_logging.py

+105-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries
2+
# SPDX-FileCopyrightText: 2024 Pat Satyshur
23
#
34
# SPDX-License-Identifier: MIT
45

@@ -58,9 +59,11 @@
5859

5960
import time
6061
import sys
62+
import os
6163
from collections import namedtuple
6264

6365
try:
66+
# pylint: disable=deprecated-class
6467
from typing import Optional, Hashable
6568
from typing_extensions import Protocol
6669

@@ -214,6 +217,8 @@ class FileHandler(StreamHandler):
214217

215218
def __init__(self, filename: str, mode: str = "a") -> None:
216219
# pylint: disable=consider-using-with
220+
if mode == "r":
221+
raise ValueError("Can't write to a read only file")
217222
super().__init__(open(filename, mode=mode))
218223

219224
def close(self) -> None:
@@ -229,13 +234,112 @@ def format(self, record: LogRecord) -> str:
229234
return super().format(record) + "\r\n"
230235

231236
def emit(self, record: LogRecord) -> None:
232-
"""Generate the message and write it to the UART.
237+
"""Generate the message and write it to the file.
233238
234239
:param record: The record (message object) to be logged
235240
"""
236241
self.stream.write(self.format(record))
237242

238243

244+
class RotatingFileHandler(FileHandler):
245+
"""File handler for writing log files to flash memory or external memory such as an SD card.
246+
This handler implements a very simple log rotating system similar to the python function of the
247+
same name (https://docs.python.org/3/library/logging.handlers.html#rotatingfilehandler)
248+
249+
If maxBytes is set, the handler will check to see if the log file is larger than the given
250+
limit. If the log file is larger than the limit, it is renamed and a new file is started.
251+
The old log file will be renamed with a numerical appendix '.1', '.2', etc... The variable
252+
backupCount controls how many old log files to keep. For example, if the filename is 'log.txt'
253+
and backupCount is 5, you will end up with six log files: 'log.txt', 'log.txt.1', 'log.txt.3',
254+
up to 'log.txt.5' Therefore, the maximum amount of disk space the logs can use is
255+
maxBytes*(backupCount+1).
256+
257+
If either maxBytes or backupCount is not set, or set to zero, the log rotation is disabled.
258+
This will result in a single log file with a name `filename` that will grow without bound.
259+
260+
:param str filename: The filename of the log file
261+
:param str mode: Whether to write ('w') or append ('a'); default is to append
262+
:param int maxBytes: The max allowable size of the log file in bytes.
263+
:param int backupCount: The number of old log files to keep.
264+
"""
265+
266+
def __init__(
267+
self,
268+
filename: str,
269+
mode: str = "a",
270+
maxBytes: int = 0,
271+
backupCount: int = 0,
272+
) -> None:
273+
if maxBytes < 0:
274+
raise ValueError("maxBytes must be a positive number")
275+
if backupCount < 0:
276+
raise ValueError("backupCount must be a positive number")
277+
278+
self._LogFileName = filename
279+
self._WriteMode = mode
280+
self._maxBytes = maxBytes
281+
self._backupCount = backupCount
282+
283+
# Open the file and save the handle to self.stream
284+
super().__init__(self._LogFileName, mode=self._WriteMode)
285+
286+
def doRollover(self) -> None:
287+
"""Roll over the log files. This should not need to be called directly"""
288+
# At this point, we have already determined that we need to roll the log files.
289+
# Close the log file. Probably needed if we want to delete/rename files.
290+
self.close()
291+
292+
for i in range(self._backupCount, 0, -1):
293+
CurrentFileName = self._LogFileName + "." + str(i)
294+
CurrentFileNamePlus = self._LogFileName + "." + str(i + 1)
295+
try:
296+
if i == self._backupCount:
297+
# This is the oldest log file. Delete this one.
298+
os.remove(CurrentFileName)
299+
else:
300+
# Rename the current file to the next number in the sequence.
301+
os.rename(CurrentFileName, CurrentFileNamePlus)
302+
except OSError as e:
303+
if e.args[0] == 2:
304+
# File does not exsist. This is okay.
305+
pass
306+
else:
307+
raise e
308+
309+
# Rename the current log to the first backup
310+
os.rename(self._LogFileName, CurrentFileName)
311+
312+
# Reopen the file.
313+
# pylint: disable=consider-using-with
314+
self.stream = open(self._LogFileName, mode=self._WriteMode)
315+
316+
def GetLogSize(self) -> int:
317+
"""Check the size of the log file."""
318+
try:
319+
self.stream.flush() # We need to call this or the file size is always zero.
320+
LogFileSize = os.stat(self._LogFileName)[6]
321+
except OSError as e:
322+
if e.args[0] == 2:
323+
# Log file does not exsist. This is okay.
324+
LogFileSize = None
325+
else:
326+
raise e
327+
return LogFileSize
328+
329+
def emit(self, record: LogRecord) -> None:
330+
"""Generate the message and write it to the file.
331+
332+
:param record: The record (message object) to be logged
333+
"""
334+
if (
335+
(self.GetLogSize() >= self._maxBytes)
336+
and (self._maxBytes > 0)
337+
and (self._backupCount > 0)
338+
):
339+
self.doRollover()
340+
self.stream.write(self.format(record))
341+
342+
239343
class NullHandler(Handler):
240344
"""Provide an empty log handler.
241345

0 commit comments

Comments
 (0)