Skip to content

Commit ee0a031

Browse files
committed
Introduce a helper to reduce logs in frequently invoked codepaths.
1 parent cbe100a commit ee0a031

3 files changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
import inspect
19+
import time
20+
21+
_LAST_TIME_INVOCATION_ALLOWED_NS = {}
22+
23+
24+
def _allow_infrequent_invocation(
25+
min_interval_sec: int, periodic_action_id: str):
26+
27+
if periodic_action_id in _LAST_TIME_INVOCATION_ALLOWED_NS:
28+
last_observed_ns = _LAST_TIME_INVOCATION_ALLOWED_NS[periodic_action_id]
29+
if time.time_ns() < last_observed_ns + min_interval_sec * 10**9:
30+
return False
31+
_LAST_TIME_INVOCATION_ALLOWED_NS[periodic_action_id] = time.time_ns()
32+
return True
33+
34+
35+
def allow_infrequent_logging(
36+
min_interval_sec: int = 5 * 60, message_id: str = None):
37+
"""Checks whether to allow printing a log message every so often.
38+
39+
Sample usages:
40+
```
41+
if logs.allow_infrequent_logging():
42+
_LOGGER.info("A message to log no more than once in 5 min per process")
43+
44+
if logs.allow_infrequent_logging(min_interval_sec=20*60,
45+
message_id="Data plane debug logs"):
46+
_LOGGER.info("Waiting to receive elements in input queue.")
47+
48+
if logs.allow_infrequent_logging(min_interval_sec=20*60,
49+
message_id="Data plane debug logs"):
50+
_LOGGER.info("Received elements in input queue.")
51+
52+
```
53+
54+
Args:
55+
min_interval_sec: Minimal time interval to wait between logs, in seconds.
56+
message_id: Optional identifier of a log message. If not provided, the
57+
identifier is derived from the location of the caller.
58+
Do not include the message being logged if it can be large.
59+
60+
Returns:
61+
True, if a log message should be produced, False otherwise.
62+
"""
63+
if not message_id:
64+
# Use a location of the code where the helper was invoked.
65+
cf = inspect.currentframe()
66+
message_id = f"{cf.f_back.f_code.co_filename}:{cf.f_back.f_lineno}"
67+
68+
return _allow_infrequent_invocation(min_interval_sec, message_id)
69+
70+
71+
def allow_log_once(message_id: str = None):
72+
"""Checks whether to allow logging a message only once per process.
73+
74+
Args:
75+
message_id: An identifier of a log message. If not provided, the
76+
identifier is derived from the location of the caller.
77+
78+
Returns:
79+
True, if a log message should be produced, False otherwise.
80+
81+
Sample usage:
82+
```
83+
if logs.allow_log_once():
84+
_LOGGER.info("Some message to log no more than once per process")
85+
```
86+
87+
See also `allow_infrequent_logging` for logging a message every so often.
88+
"""
89+
if not message_id:
90+
# Use a location of the code where the helper was invoked.
91+
cf = inspect.currentframe()
92+
message_id = f"{cf.f_back.f_code.co_filename}:{cf.f_back.f_lineno}"
93+
94+
return _allow_infrequent_invocation(
95+
min_interval_sec=10**10, periodic_action_id=message_id)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
import uuid
19+
import unittest
20+
21+
from apache_beam.utils import logs
22+
from unittest import mock
23+
24+
25+
class LogsTest(unittest.TestCase):
26+
def test_allow_infrequent_logging_always_allows_if_interval_is_zero(self):
27+
random_id = str(uuid.uuid4())
28+
self.assertTrue(
29+
logs.allow_infrequent_logging(min_interval_sec=0, message_id=random_id))
30+
self.assertTrue(
31+
logs.allow_infrequent_logging(min_interval_sec=0, message_id=random_id))
32+
33+
def test_allow_infrequent_logging_allows_no_message_id(self):
34+
self.assertTrue(logs.allow_infrequent_logging(min_interval_sec=0))
35+
36+
def test_allow_infrequent_logging_prohibit_sequential_logs_with_same_id(self):
37+
random_id = str(uuid.uuid4())
38+
with mock.patch("time.time_ns") as mock_time:
39+
mock_time.return_value = 0 # Initial time
40+
self.assertTrue(
41+
logs.allow_infrequent_logging(
42+
min_interval_sec=1000, message_id=random_id))
43+
mock_time.return_value = 1 * 1_000_000_000 # 1 second.
44+
self.assertFalse(
45+
logs.allow_infrequent_logging(
46+
min_interval_sec=1000, message_id=random_id))
47+
mock_time.return_value = 2000 * 1_000_000_000 # 2000 seconds.
48+
self.assertTrue(
49+
logs.allow_infrequent_logging(
50+
min_interval_sec=1000, message_id=random_id))
51+
52+
def test_allow_log_once_allows_only_once(self):
53+
random_id = str(uuid.uuid4())
54+
self.assertTrue(logs.allow_log_once(message_id=random_id))
55+
self.assertFalse(logs.allow_log_once(message_id=random_id))
56+
57+
58+
if __name__ == '__main__':
59+
unittest.main()

sdks/python/scripts/generate_pydoc.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ excluded_patterns=(
7979
'apache_beam/transforms/cy_dataflow_distribution_counter.*'
8080
'apache_beam/transforms/py_dataflow_distribution_counter.*'
8181
'apache_beam/utils/counters.*'
82+
'apache_beam/utils/logs.*'
8283
'apache_beam/utils/windowed_value.*'
8384
'apache_beam/version.py'
8485
'apache_beam/yaml/integration_tests.py'

0 commit comments

Comments
 (0)