Skip to content

Commit 7638420

Browse files
committed
implement rotating file logger
1 parent 587325c commit 7638420

4 files changed

Lines changed: 103 additions & 5 deletions

File tree

can/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class CanError(IOError):
2424

2525
from .listener import Listener, BufferedReader, RedirectReader, AsyncBufferedReader
2626

27-
from .io import Logger, Printer, LogReader, MessageSync
27+
from .io import Logger, RotatingFileLogger, Printer, LogReader, MessageSync
2828
from .io import ASCWriter, ASCReader
2929
from .io import BLFReader, BLFWriter
3030
from .io import CanutilsLogReader, CanutilsLogWriter

can/io/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
# Generic
7-
from .logger import Logger
7+
from .logger import Logger, RotatingFileLogger
88
from .player import LogReader, MessageSync
99

1010
# Format specific

can/io/logger.py

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
"""
44

55
import pathlib
6-
import typing
6+
from typing import Optional, Callable
77

88
from pkg_resources import iter_entry_points
99
import can.typechecking
1010

11+
from ..message import Message
1112
from ..listener import Listener
12-
from .generic import BaseIOHandler
13+
from .generic import BaseIOHandler, MessageWriter
1314
from .asc import ASCWriter
1415
from .blf import BLFWriter
1516
from .canutils import CanutilsLogWriter
@@ -51,7 +52,7 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method
5152

5253
@staticmethod
5354
def __new__(
54-
cls, filename: typing.Optional[can.typechecking.StringPathLike], *args, **kwargs
55+
cls, filename: Optional[can.typechecking.StringPathLike], *args, **kwargs
5556
):
5657
"""
5758
:param filename: the filename/path of the file to write to,
@@ -78,3 +79,97 @@ def __new__(
7879
raise ValueError(
7980
f'No write support for this unknown log format "{suffix}"'
8081
) from None
82+
83+
84+
class RotatingFileLogger(Listener):
85+
"""Log CAN messages to a sequence of files with a given maximum size.
86+
87+
The log file name and path must be returned by the function`filename_func`
88+
as a path-like object (e.g. a string). The log file format is defined by
89+
the suffix of the path-like object.
90+
91+
The RotatingFileLogger currently supports
92+
* .asc: :class:`can.ASCWriter`
93+
* .blf :class:`can.BLFWriter`
94+
* .csv: :class:`can.CSVWriter`
95+
* .log :class:`can.CanutilsLogWriter`
96+
* .txt :class:`can.Printer`
97+
98+
The log files may be incomplete until `stop()` is called due to buffering.
99+
100+
Example::
101+
102+
from can import Notifier
103+
from can.interfaces.vector import VectorBus
104+
from can import RotatingFileLogger
105+
106+
bus = VectorBus(channel=[0], app_name="CANape", fd=True)
107+
logger = RotatingFileLogger(
108+
filename_func=lambda idx: f"CAN_Log_{idx:03}.txt", # filename with three digit counter
109+
max_bytes=5 * 1024 ** 2, # =5MB
110+
initial_file_number=23, # start with number 23
111+
)
112+
notifier = Notifier(bus=bus, listeners=[logger])
113+
114+
"""
115+
116+
supported_writers = {
117+
".asc": ASCWriter,
118+
".blf": BLFWriter,
119+
".csv": CSVWriter,
120+
".log": CanutilsLogWriter,
121+
".txt": Printer,
122+
}
123+
124+
def __init__(
125+
self,
126+
filename_func: Callable[[int], can.typechecking.StringPathLike],
127+
max_bytes: int,
128+
initial_file_number: int = 0,
129+
*args,
130+
**kwargs,
131+
):
132+
"""
133+
:param filename_func:
134+
A function or lambda expression that returns the path of the new
135+
log file e.g. `lambda file_number: f"C:\\my_can_logfile_{file_number:03}.asc"`.
136+
:param max_bytes:
137+
The file size threshold in bytes at which a new file is created.
138+
:param initial_file_number:
139+
The first log file will start with number `initial_file_number`.
140+
:raises ValueError:
141+
The filename's suffix is not supported.
142+
"""
143+
self.filename_func = filename_func
144+
self.max_bytes = max_bytes
145+
self.file_number = initial_file_number
146+
147+
self.writer_args = args
148+
self.writer_kwargs = kwargs
149+
150+
self.writer: MessageWriter = self._get_new_writer()
151+
152+
def on_message_received(self, msg: Message):
153+
if self.writer.file is not None and self.writer.file.tell() >= self.max_bytes:
154+
self.writer.stop()
155+
self.file_number += 1
156+
self.writer = self._get_new_writer()
157+
158+
self.writer.on_message_received(msg)
159+
160+
def on_error(self, exc: Exception):
161+
self.writer.on_error(exc)
162+
163+
def stop(self):
164+
self.writer.stop()
165+
166+
def _get_new_writer(self) -> MessageWriter:
167+
filename = self.filename_func(self.file_number)
168+
suffix = pathlib.Path(filename).suffix.lower()
169+
try:
170+
writer_class = self.supported_writers[suffix]
171+
except KeyError:
172+
raise ValueError(
173+
f'Log format "{suffix} is not supported by RotatingFileLogger."'
174+
)
175+
return writer_class(filename, *self.writer_args, **self.writer_kwargs)

doc/listeners.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ create log files with different file types of the messages received.
7575
.. autoclass:: can.Logger
7676
:members:
7777

78+
.. autoclass:: can.RotatingFileLogger
79+
:members:
80+
7881

7982
Printer
8083
-------

0 commit comments

Comments
 (0)