Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize the RotatingLogger; size, time or first of the two trigger the rollover #1365

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5c23248
Generalize the RotatingLogger; size, time or both constraint
j-c-cook Aug 7, 2022
7970ce1
Merge branch 'develop' into issue1364_TimedRotatingLogger
j-c-cook Aug 11, 2022
019e41c
Update test_log_virtual_sizedlogger to have a file path
j-c-cook Aug 11, 2022
e091ba0
Update logger doc string
j-c-cook Aug 11, 2022
5be3a81
Move rollover time reset to capture when file_size triggers
j-c-cook Aug 11, 2022
89bf921
Merge branch 'develop' into issue1364_TimedRotatingLogger
Sep 29, 2022
0cf8383
Change `delta_t` to `max_seconds`
Sep 29, 2022
cd36995
Run logger test on `RotatingLogger` rather than `SizedRotatingLogger`
Sep 29, 2022
c9ba894
Modify timeout period name and call Rotating if `file_time`
Sep 29, 2022
dc75109
Add test for the timed logger given `file_time` input
Sep 29, 2022
22bf537
Update logger help statement
Sep 29, 2022
83b901e
Merge branch 'develop' into issue1364_TimedRotatingLogger
j-c-cook Dec 20, 2022
0a89a6c
Merge branch 'develop' into issue1364_TimedRotatingLogger
j-c-cook Dec 27, 2022
c3d8715
Update can/logger.py
j-c-cook Dec 28, 2022
b6b75b9
Adjust the example in docstring and update documentation
j-c-cook Dec 28, 2022
a830c7a
Update the deprecation warning message
j-c-cook Dec 28, 2022
2862573
Fix doc string build warning
j-c-cook Dec 28, 2022
78f777c
Merge branch 'develop' into issue1364_TimedRotatingLogger
j-c-cook Dec 28, 2022
b8a7ec5
Update API order to be like develop and add formats set
j-c-cook Dec 28, 2022
ce815d6
Fix mypy workflow build error check
j-c-cook Dec 28, 2022
842dcbb
Include the previous changes made to _default_name
j-c-cook Dec 28, 2022
2a466b9
Fix spacing in _default_name
j-c-cook Dec 28, 2022
49649e7
Update logconvert to use the generalized RotatingLogger
j-c-cook Dec 28, 2022
35d25c7
Fix comment to reference RotatingLogger
j-c-cook Dec 28, 2022
b763f4a
Format code with black
j-c-cook Dec 28, 2022
29717f4
Add test for time rollover condition
j-c-cook Dec 28, 2022
ad382b5
Merge branch 'issue1364_TimedRotatingLogger' of https://github.com/j-…
j-c-cook Dec 28, 2022
8aff574
Add note to preserve size rotating test functions at v5.0
j-c-cook Dec 28, 2022
cabafb4
Merge branch 'develop' into issue1364_TimedRotatingLogger
j-c-cook Jan 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion can/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@
from .interface import Bus, detect_available_configs
from .bit_timing import BitTiming

from .io import Logger, SizedRotatingLogger, Printer, LogReader, MessageSync
from .io import (
Logger,
SizedRotatingLogger,
Printer,
LogReader,
MessageSync,
RotatingLogger,
)
felixdivo marked this conversation as resolved.
Show resolved Hide resolved
from .io import ASCWriter, ASCReader
from .io import BLFReader, BLFWriter
from .io import CanutilsLogReader, CanutilsLogWriter
Expand Down
2 changes: 1 addition & 1 deletion can/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

# Generic
from .logger import Logger, BaseRotatingLogger, SizedRotatingLogger
from .logger import Logger, BaseRotatingLogger, SizedRotatingLogger, RotatingLogger
from .player import LogReader, MessageSync

# Format specific
Expand Down
142 changes: 127 additions & 15 deletions can/io/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import pathlib
from abc import ABC, abstractmethod
from datetime import datetime
import time
import warnings
import gzip
from typing import Any, Optional, Callable, Type, Tuple, cast, Dict, Set

Expand Down Expand Up @@ -272,11 +274,14 @@ def do_rollover(self) -> None:
"""Perform rollover."""


class SizedRotatingLogger(BaseRotatingLogger):
"""Log CAN messages to a sequence of files with a given maximum size.
class RotatingLogger(BaseRotatingLogger):
"""Log CAN messages to a sequence of files with a given maximum size,
a specified amount of time or a combination of both. When both max size
(bytes) and rollover time (seconds) are provided, the rollover trigger
is caused by the first to occur.

The logger creates a log file with the given `base_filename`. When the
size threshold is reached the current log file is closed and renamed
The logger creates a log file with the given `base_filename`. When a size
or time threshold is reached the current log file is closed and renamed
by adding a timestamp and the rollover count. A new log file is then
created and written to.

Expand All @@ -287,20 +292,38 @@ class SizedRotatingLogger(BaseRotatingLogger):

Example::
j-c-cook marked this conversation as resolved.
Show resolved Hide resolved

from can import Notifier, SizedRotatingLogger
from can import Notifier, RotatingLogger
from can.interfaces.vector import VectorBus

bus = VectorBus(channel=[0], app_name="CANape", fd=True)

logger = SizedRotatingLogger(
# Size constrained rollover
logger = RotatingLogger(
base_filename="my_logfile.asc",
max_bytes=5 * 1024 ** 2, # =5MB
max_bytes=5 * 1024 ** 2, # = 5 MB
)
logger.rollover_count = 23 # start counter at 23

notifier = Notifier(bus=bus, listeners=[logger])

The SizedRotatingLogger currently supports the formats
Example::

# Size or time constrained rollover (whichever limit occurs first)
logger = RotatingLogger(
base_filename="my_logfile.asc",
max_bytes=5 * 1024 ** 2, # = 5 MB
max_seconds=5 * 60, # = 5 minutes
)

Example::

# Time constrained rollover
logger = RotatingLogger(
base_filename="my_logfile.asc",
max_seconds=5 * 60, # = 5 minutes
)

The RotatingLogger currently supports the formats
* .asc: :class:`can.ASCWriter`
* .blf :class:`can.BLFWriter`
* .csv: :class:`can.CSVWriter`
Expand All @@ -315,41 +338,60 @@ class SizedRotatingLogger(BaseRotatingLogger):
"""

_supported_formats = {".asc", ".blf", ".csv", ".log", ".txt"}
last_rollover_time = time.time()

def __init__(
self,
base_filename: StringPathLike,
max_bytes: int = 0,
max_seconds: int = 0,
**kwargs: Any,
) -> None:
"""
:param base_filename:
A path-like object for the base filename. The log file format is defined by
the suffix of `base_filename`.
A path-like object for the base filename. The log file format is
defined by the suffix of `base_filename`.
:param max_bytes:
The size threshold at which a new log file shall be created. If set to 0, no
rollover will be performed.
The size threshold at which a new log file shall be created. If set
less than or equal to 0, no rollover will be performed.
:param max_seconds:
The elapsed time threshold at which a new log file shall be
created. If set less than or equal to 0, no rollover will be
performed.
"""
super().__init__(**kwargs)

self.base_filename = os.path.abspath(base_filename)
self.max_bytes = max_bytes

# Rotation parameters
self.max_bytes = max_bytes # Maximum bytes for rotation (bytes)
self.max_seconds = max_seconds # Time difference between rotation (seconds)

self._writer = self._get_new_writer(self.base_filename)

def should_rollover(self, msg: Message) -> bool:
if self.max_bytes <= 0:
# Check to see if a file rollover should occur based on file size
# (bytes) and elapsed time (seconds) since last rollover.
if self.max_bytes <= 0 and self.max_seconds <= 0:
return False

if self.writer.file_size() >= self.max_bytes:
# Check to see if the file size is greater than max bytes
if self.writer.file_size() >= self.max_bytes > 0:
return True
# Check to see if elapsed time is greater than max seconds
if time.time() - self.last_rollover_time > self.max_seconds > 0:
return True

return False

def do_rollover(self) -> None:
# Perform the file rollover.
if self.writer:
self.writer.stop()

# Reset the time since last rollover
self.last_rollover_time = time.time()

sfn = self.base_filename
dfn = self.rotation_filename(self._default_name())
self.rotate(sfn, dfn)
Expand All @@ -368,3 +410,73 @@ def _default_name(self) -> StringPathLike:
+ "".join(path.suffixes[-2:])
)
return str(path.parent / new_name)


class SizedRotatingLogger(RotatingLogger):
"""Log CAN messages to a sequence of files with a given maximum size.

The logger creates a log file with the given `base_filename`. When the
size threshold is reached the current log file is closed and renamed
by adding a timestamp and the rollover count. A new log file is then
created and written to.

This behavior can be customized by setting the
:attr:`~can.io.BaseRotatingLogger.namer` and
:attr:`~can.io.BaseRotatingLogger.rotator`
attribute.

Example::

from can import Notifier, SizedRotatingLogger
from can.interfaces.vector import VectorBus

bus = VectorBus(channel=[0], app_name="CANape", fd=True)

logger = SizedRotatingLogger(
base_filename="my_logfile.asc",
max_bytes=5 * 1024 ** 2, # =5MB
)
logger.rollover_count = 23 # start counter at 23

notifier = Notifier(bus=bus, listeners=[logger])

The SizedRotatingLogger currently supports the formats
* .asc: :class:`can.ASCWriter`
* .blf :class:`can.BLFWriter`
* .csv: :class:`can.CSVWriter`
* .log :class:`can.CanutilsLogWriter`
* .txt :class:`can.Printer` (if pointing to a file)

.. note::
The :class:`can.SqliteWriter` is not supported yet.

The log files on disk may be incomplete due to buffering until
:meth:`~can.Listener.stop` is called.
"""

_supported_formats = {".asc", ".blf", ".csv", ".log", ".txt"}

def __init__(
self,
base_filename: StringPathLike,
max_bytes: int = 0,
**kwargs: Any,
) -> None:
"""
:param base_filename:
A path-like object for the base filename. The log file format is defined by
the suffix of `base_filename`.
:param max_bytes:
The size threshold at which a new log file shall be created. If set to 0, no
rollover will be performed.
"""
# This object is deprecated as of 4.2 and will be removed in 5.0.
warnings.simplefilter("always", DeprecationWarning)
warnings.warn(
"`can.io.SizedRotatingLogger` is deprecated as of v4.2. It will be "
"removed in v5.0. New features are not supported by this object. "
"Use the `can.io.RotatingLogger`. class instead",
DeprecationWarning,
)
# Initialize self as a RotatingLogger
super().__init__(base_filename, max_bytes, **kwargs)
6 changes: 2 additions & 4 deletions can/logconvert.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import argparse
import errno

from can import LogReader, Logger, SizedRotatingLogger
from can import LogReader, Logger, RotatingLogger


class ArgumentParser(argparse.ArgumentParser):
Expand Down Expand Up @@ -48,9 +48,7 @@ def main():
with LogReader(args.input) as reader:

if args.file_size:
logger = SizedRotatingLogger(
base_filename=args.output, max_bytes=args.file_size
)
logger = RotatingLogger(base_filename=args.output, max_bytes=args.file_size)
else:
logger = Logger(filename=args.output)

Expand Down
18 changes: 15 additions & 3 deletions can/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import can
from can.io import BaseRotatingLogger
from can.io.generic import MessageWriter
from . import Bus, BusState, Logger, SizedRotatingLogger
from . import Bus, BusState, Logger, RotatingLogger
from .typechecking import CanFilter, CanFilters


Expand Down Expand Up @@ -185,6 +185,17 @@ def main() -> None:
default=None,
)

parser.add_argument(
"-t",
"--file_time",
dest="file_time",
type=int,
help="Maximum period in seconds before rotating log file. (If file_size "
"is also given, then the first of the two constraints to occur is "
"what causes the rollover.)",
default=0,
)

parser.add_argument(
"-v",
action="count",
Expand Down Expand Up @@ -224,10 +235,11 @@ def main() -> None:
print(f"Can Logger (Started on {datetime.now()})")

logger: Union[MessageWriter, BaseRotatingLogger]
if results.file_size:
logger = SizedRotatingLogger(
if results.file_size or results.file_time:
logger = RotatingLogger(
base_filename=results.log_file,
max_bytes=results.file_size,
max_seconds=results.file_time,
append=results.append,
**additional_config,
)
Expand Down
2 changes: 1 addition & 1 deletion doc/listeners.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ create log files with different file types of the messages received.
.. autoclass:: can.io.BaseRotatingLogger
:members:

.. autoclass:: can.SizedRotatingLogger
.. autoclass:: can.RotatingLogger
:members:


Expand Down
16 changes: 13 additions & 3 deletions test/test_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ def setUp(self) -> None:
self.MockLoggerUse = self.MockLogger
self.loggerToUse = self.mock_logger

# Patch SizedRotatingLogger object
patcher_logger_sized = mock.patch("can.logger.SizedRotatingLogger", spec=True)
# Patch RotatingLogger object
patcher_logger_sized = mock.patch("can.logger.RotatingLogger", spec=True)
self.MockLoggerSized = patcher_logger_sized.start()
self.addCleanup(patcher_logger_sized.stop)
self.mock_logger_sized = self.MockLoggerSized.return_value
Expand Down Expand Up @@ -103,7 +103,17 @@ def test_log_virtual_sizedlogger(self):
self.MockLoggerUse = self.MockLoggerSized
self.loggerToUse = self.mock_logger_sized

sys.argv = self.baseargs + ["--file_size", "1000000"]
sys.argv = self.baseargs + ["-f file.log"] + ["--file_size", "1000000"]
can.logger.main()
self.assertSuccessfullCleanup()
self.mock_logger_sized.assert_called_once()

def test_log_virtual_timedlogger(self):
self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt])
self.MockLoggerUse = self.MockLoggerSized
self.loggerToUse = self.mock_logger_sized

sys.argv = self.baseargs + ["-f file.log"] + ["--file_time", "5"]
can.logger.main()
self.assertSuccessfullCleanup()
self.mock_logger_sized.assert_called_once()
Expand Down
Loading