Skip to content

Commit c5d7f8f

Browse files
committed
Allow for lazy-evaluation of arguments in LazyFormat (#1669)
When creating a `LazyFormat` object, the keyword argument values may be constructed from expensive methods. Similar to allowing a function to be passed in for the `message` argument, this PR enables methods to be passed in for the keyword argument values so that they can be lazily evaluated. There were some logging calls that were found in performance profiling to be expensive because lazy-evaluation was not done, so this PR updates a few of those call sites as well. An alternative approach would have been to implement `MetricFlowPrettyFormattable` for those objects.
1 parent e991c5e commit c5d7f8f

File tree

4 files changed

+52
-10
lines changed

4 files changed

+52
-10
lines changed

metricflow-semantics/metricflow_semantics/mf_logging/lazy_formattable.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from __future__ import annotations
22

3+
import logging
34
from functools import cached_property
45
from typing import Callable, Union
56

67
from typing_extensions import override
78

89
from metricflow_semantics.mf_logging.pretty_print import mf_pformat_dict
910

11+
logger = logging.getLogger(__name__)
12+
1013

1114
class LazyFormat:
1215
"""Lazily formats the given objects into a string representation for logging.
@@ -40,7 +43,8 @@ def __init__(self, message: Union[str, Callable[[], str]], **kwargs) -> None: #
4043
4144
Args:
4245
message: The message or a function that returns a message.
43-
**kwargs: A dictionary of objects to format to text when `__str__` is called on this object.
46+
**kwargs: A dictionary of objects to format to text when `__str__` is called on this object. To allow for
47+
lazy evaluation of argument values, a callable that takes no arguments can be supplied for a key's value.
4448
"""
4549
self._message = message
4650
self._kwargs = kwargs
@@ -52,7 +56,23 @@ def _str_value(self) -> str:
5256
message = self._message()
5357
else:
5458
message = self._message
55-
return mf_pformat_dict(message, self._kwargs, preserve_raw_strings=True)
59+
60+
evaluated_args = {}
61+
for arg_name, arg_value in self._kwargs.items():
62+
if callable(arg_value):
63+
try:
64+
evaluated_args[arg_name] = arg_value()
65+
continue
66+
except Exception:
67+
logger.warning(
68+
f"Got an exception while evaluating {arg_name=} {arg_value=}. Since this is an error with "
69+
f"formatting log output, this should not result in system issues. However, the exception "
70+
f"indicates a bug with how the logging call is made and should be investigated.",
71+
exc_info=True,
72+
)
73+
74+
evaluated_args[arg_name] = arg_value
75+
return mf_pformat_dict(message, evaluated_args, preserve_raw_strings=True)
5676

5777
@override
5878
def __str__(self) -> str:

metricflow-semantics/tests_metricflow_semantics/mf_logging/test_lazy_format.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
logger = logging.getLogger(__name__)
1111

12+
_NOT_FORMATTED_ASSERTION_MESSAGE = "This should not have been formatted as a string."
13+
1214

1315
def test_log_kwargs() -> None:
1416
"""Test that objects included via keyword args are formatted."""
@@ -36,12 +38,11 @@ def test_lambda() -> None:
3638

3739
def test_lazy_object() -> None:
3840
"""Test that formatting of objects are done lazily and not when the logging level is not appropriate."""
39-
assertion_message = "This should not have been formatted as a string."
4041

4142
class _NotFormattedObject:
4243
@override
4344
def __repr__(self) -> str:
44-
raise AssertionError(assertion_message)
45+
raise AssertionError(_NOT_FORMATTED_ASSERTION_MESSAGE)
4546

4647
# Logging level is INFO, so DEBUG messages shouldn't be logged / formatted.
4748
recorded_logger: logging.Logger
@@ -51,14 +52,35 @@ def __repr__(self) -> str:
5152

5253

5354
def test_lazy_lambda() -> None:
54-
"""Test that a lambda input is not evaluated when the logging level is not appropriate."""
55-
assertion_message = "This should not have been formatted as a string."
55+
"""Test that a lambda message is not evaluated when the logging level is not appropriate."""
5656

5757
def _should_not_be_called() -> str:
58-
raise AssertionError(assertion_message)
58+
raise AssertionError(_NOT_FORMATTED_ASSERTION_MESSAGE)
5959

6060
# Logging level is INFO, so DEBUG messages shouldn't be logged / formatted.
6161
recorded_logger: logging.Logger
6262
handler: RecordingLogHandler
6363
with recorded_logging_context(logger, logging.INFO) as (recorded_logger, handler):
6464
recorded_logger.debug(LazyFormat(lambda: f"{_should_not_be_called()}"))
65+
66+
67+
def test_lambda_argument() -> None:
68+
"""Tests that a callable that's supplied as an argument value is evaluated."""
69+
recorded_logger: logging.Logger
70+
handler: RecordingLogHandler
71+
with recorded_logging_context(logger, logging.INFO) as (recorded_logger, handler):
72+
recorded_logger.info(LazyFormat("Example message", arg_0=lambda: 1))
73+
assert handler.get_last_message() == "Example message (arg_0=1)"
74+
75+
76+
def test_lazy_lambda_argument() -> None:
77+
"""Test that a lambda input is not evaluated when the logging level is not appropriate."""
78+
79+
def _should_not_be_called() -> str:
80+
raise AssertionError(_NOT_FORMATTED_ASSERTION_MESSAGE)
81+
82+
# Logging level is INFO, so DEBUG messages shouldn't be logged / formatted.
83+
recorded_logger: logging.Logger
84+
handler: RecordingLogHandler
85+
with recorded_logging_context(logger, logging.INFO) as (recorded_logger, handler):
86+
recorded_logger.debug(LazyFormat("Example message", arg_0=lambda: f"{_should_not_be_called()}"))

metricflow/dataflow/optimizer/source_scan/source_scan_optimizer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,8 @@ def optimize(self, dataflow_plan: DataflowPlan) -> DataflowPlan: # noqa: D102
326326
logger.debug(
327327
LazyFormat(
328328
"Optimized dataflow plan",
329-
original_plan=dataflow_plan.sink_node.structure_text(),
330-
optimized_plan=optimized_result.optimized_branch.structure_text(),
329+
original_plan=dataflow_plan.sink_node.structure_text,
330+
optimized_plan=optimized_result.optimized_branch.structure_text,
331331
)
332332
)
333333

metricflow/sql/optimizer/column_pruner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def optimize(self, node: SqlPlanNode) -> SqlPlanNode: # noqa: D102
114114
logger.error(
115115
LazyFormat(
116116
"The columns required at this node can't be determined, so skipping column pruning",
117-
node=node.structure_text(),
117+
node=node.structure_text,
118118
required_select_columns=required_select_columns,
119119
)
120120
)

0 commit comments

Comments
 (0)