Skip to content

Commit d02f5f0

Browse files
author
David Conner
committed
Add throttle option for logging (PR #14)
1 parent bf0cb3e commit d02f5f0

File tree

3 files changed

+413
-15
lines changed

3 files changed

+413
-15
lines changed

flexbe_core/flexbe_core/logger.py

+84-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python
22

3-
# Copyright 2023 Philipp Schillinger, Team ViGIR, Christopher Newport University
3+
# Copyright 2024 Philipp Schillinger, Team ViGIR, Christopher Newport University
44
#
55
# Redistribution and use in source and binary forms, with or without
66
# modification, are permitted provided that the following conditions are met:
@@ -31,7 +31,9 @@
3131

3232
"""Realize behavior-specific logging."""
3333

34+
from rclpy.exceptions import ParameterNotDeclaredException
3435
from rclpy.node import Node
36+
from rclpy.duration import Duration
3537

3638
from flexbe_msgs.msg import BehaviorLog
3739

@@ -47,13 +49,37 @@ class Logger:
4749

4850
LOGGING_TOPIC = 'flexbe/log'
4951

52+
# max number of items in last logged dict (used for log_throttle)
53+
MAX_LAST_LOGGED_SIZE = 1024
54+
LAST_LOGGED_CLEARING_RATIO = 0.2
55+
5056
_pub = None
5157
_node = None
5258

5359
@staticmethod
5460
def initialize(node: Node):
5561
Logger._node = node
5662
Logger._pub = node.create_publisher(BehaviorLog, Logger.LOGGING_TOPIC, 100)
63+
Logger._last_logged = {}
64+
65+
# Optional parameters that can be defined
66+
try:
67+
size_param = node.get_parameter("max_throttle_logging_size")
68+
if size_param.type_ == size_param.Type.INTEGER:
69+
Logger.MAX_LAST_LOGGED_SIZE = size_param.value
70+
except ParameterNotDeclaredException as exc:
71+
pass
72+
73+
try:
74+
clear_param = node.get_parameter("throttle_logging_clear_ratio")
75+
if clear_param.type_ in (clear_param.Type.INTEGER, clear_param.Type.DOUBLE):
76+
Logger.LAST_LOGGED_CLEARING_RATIO = clear_param.value
77+
except ParameterNotDeclaredException as exc:
78+
pass
79+
80+
Logger._node.get_logger().debug(f"Enable throttle logging option with "
81+
f"max size={Logger.MAX_LAST_LOGGED_SIZE} "
82+
f"clear ratio={Logger.LAST_LOGGED_CLEARING_RATIO}")
5783

5884
@staticmethod
5985
def log(text: str, severity: int):
@@ -67,6 +93,25 @@ def log(text: str, severity: int):
6793
# also log locally
6894
Logger.local(text, severity)
6995

96+
@staticmethod
97+
def log_throttle(period : float, text: str, severity : int):
98+
# create unique identifier for each logging message
99+
log_id = f"{severity}_{text}"
100+
time_now = Logger._node.get_clock().now()
101+
# only log when it's the first time or period time has passed for the logging message
102+
if not log_id in Logger._last_logged.keys() or \
103+
time_now - Logger._last_logged[log_id] > Duration(seconds=period):
104+
Logger.log(text, severity)
105+
Logger._last_logged.update({log_id: time_now})
106+
107+
if len(Logger._last_logged) > Logger.MAX_LAST_LOGGED_SIZE:
108+
# iterate through last logged items, sorted by the timestamp (oldest last)
109+
clear_size = Logger.MAX_LAST_LOGGED_SIZE * (1 - Logger.LAST_LOGGED_CLEARING_RATIO)
110+
for i, log in enumerate(sorted(Logger._last_logged.items(), key=lambda item: item[1], reverse=True)):
111+
# remove defined percentage of oldest items
112+
if i > clear_size:
113+
Logger._last_logged.pop(log[0])
114+
70115
@staticmethod
71116
def local(text: str, severity: int):
72117
if Logger._node is None:
@@ -87,57 +132,81 @@ def local(text: str, severity: int):
87132
# NOTE: Below text strings can only have single % symbols if they are being treated
88133
# as format strings with appropriate arguments (otherwise replace with %% for simple string without args)
89134
@staticmethod
90-
def logdebug(text, *args):
135+
def logdebug(text: str, *args):
91136
Logger.log(text % args, Logger.REPORT_DEBUG)
92137

93138
@staticmethod
94-
def loginfo(text, *args):
139+
def loginfo(text: str, *args):
95140
Logger.log(text % args, Logger.REPORT_INFO)
96141

97142
@staticmethod
98-
def logwarn(text, *args):
143+
def logwarn(text: str, *args):
99144
Logger.log(text % args, Logger.REPORT_WARN)
100145

101146
@staticmethod
102-
def loghint(text, *args):
147+
def loghint(text: str, *args):
103148
Logger.log(text % args, Logger.REPORT_HINT)
104149

105150
@staticmethod
106-
def logerr(text, *args):
151+
def logerr(text: str, *args):
107152
Logger.log(text % args, Logger.REPORT_ERROR)
108153

109154
@staticmethod
110-
def localdebug(text, *args):
155+
def logdebug_throttle(period: float, text: str, *args):
156+
Logger.log_throttle(period, text % args, Logger.REPORT_DEBUG)
157+
158+
@staticmethod
159+
def loginfo_throttle(period: float, text: str, *args):
160+
Logger.log_throttle(period, text % args, Logger.REPORT_INFO)
161+
162+
@staticmethod
163+
def logwarn_throttle(period: float, text: str, *args):
164+
Logger.log_throttle(period, text % args, Logger.REPORT_WARN)
165+
166+
@staticmethod
167+
def loghint_throttle(period: float, text: str, *args):
168+
Logger.log_throttle(period, text % args, Logger.REPORT_HINT)
169+
170+
@staticmethod
171+
def logerr_throttle(period: float, text: str, *args):
172+
Logger.log_throttle(period, text % args, Logger.REPORT_ERROR)
173+
174+
@staticmethod
175+
def localdebug(text: str, *args):
111176
Logger.local(text % args, Logger.REPORT_DEBUG)
112177

113178
@staticmethod
114-
def localinfo(text, *args):
179+
def localinfo(text: str, *args):
115180
Logger.local(text % args, Logger.REPORT_INFO)
116181

117182
@staticmethod
118-
def localwarn(text, *args):
183+
def localwarn(text: str, *args):
119184
Logger.local(text % args, Logger.REPORT_WARN)
120185

121186
@staticmethod
122-
def localerr(text, *args):
187+
def localhint(text: str, *args):
188+
Logger.local(text % args, Logger.REPORT_HINT)
189+
190+
@staticmethod
191+
def localerr(text: str, *args):
123192
Logger.local(text % args, Logger.REPORT_ERROR)
124193

125194
@staticmethod
126-
def debug(text, *args):
195+
def debug(text: str, *args):
127196
Logger.logdebug(text, *args)
128197

129198
@staticmethod
130-
def info(text, *args):
199+
def info(text: str, *args):
131200
Logger.loginfo(text, *args)
132201

133202
@staticmethod
134-
def warning(text, *args):
203+
def warning(text: str, *args):
135204
Logger.logwarn(text, *args)
136205

137206
@staticmethod
138-
def hint(text, *args):
207+
def hint(text: str, *args):
139208
Logger.loghint(text, *args)
140209

141210
@staticmethod
142-
def error(text, *args):
211+
def error(text: str, *args):
143212
Logger.logerr(text, *args)
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright 2024 Christopher Newport University
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# * Redistributions of source code must retain the above copyright
9+
# notice, this list of conditions and the following disclaimer.
10+
#
11+
# * Redistributions in binary form must reproduce the above copyright
12+
# notice, this list of conditions and the following disclaimer in the
13+
# documentation and/or other materials provided with the distribution.
14+
#
15+
# * Neither the name of the Philipp Schillinger, Team ViGIR, Christopher Newport University nor the names of its
16+
# contributors may be used to endorse or promote products derived from
17+
# this software without specific prior written permission.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29+
# POSSIBILITY OF SUCH DAMAGE.
30+
31+
32+
"""Test description for test proxies."""
33+
import os
34+
import sys
35+
import launch
36+
import launch_testing.actions
37+
import pytest
38+
39+
40+
@pytest.mark.rostest
41+
def generate_test_description():
42+
43+
path_to_test = os.path.dirname(__file__)
44+
45+
TEST_PROC_PATH = os.path.join(path_to_test, 'test_logger.py')
46+
47+
# This is necessary to get unbuffered output from the process under test
48+
proc_env = os.environ.copy()
49+
proc_env['PYTHONUNBUFFERED'] = '1'
50+
51+
test_logger = launch.actions.ExecuteProcess(
52+
cmd=[sys.executable, TEST_PROC_PATH],
53+
env=proc_env,
54+
output='screen',
55+
sigterm_timeout=launch.substitutions.LaunchConfiguration('sigterm_timeout', default=90),
56+
sigkill_timeout=launch.substitutions.LaunchConfiguration('sigkill_timeout', default=90)
57+
)
58+
59+
return (
60+
launch.LaunchDescription([
61+
test_logger,
62+
launch_testing.actions.ReadyToTest()
63+
]),
64+
{
65+
'test_logger': test_logger,
66+
}
67+
)

0 commit comments

Comments
 (0)