| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| import asyncio | |
| import logging | |
| import pytest | |
| from hamilton import ad_hoc_utils, async_driver, driver | |
| from hamilton.execution import executors | |
| # from hamilton.plugins.h_dask import DaskExecutor # FIXME: Not available CI (see below) | |
| from hamilton.plugins.h_logging import AsyncLoggingAdapter, LoggingAdapter, get_logger | |
| # from hamilton.plugins.h_ray import RayTaskExecutor # FIXME: Not available in the CI (see below) | |
| from hamilton.plugins.h_threadpool import FutureAdapter | |
| from . import test_logging_task_nodes | |
| def _split_log_messages(caplog, name): | |
| debug, info, warning, error = [], [], [], [] | |
| for record in caplog.records: | |
| if record.name == name: | |
| if record.levelno == logging.DEBUG: | |
| debug.append(record.message) | |
| elif record.levelno == logging.INFO: | |
| info.append(record.message) | |
| elif record.levelno == logging.WARNING: | |
| warning.append(record.message) | |
| elif record.levelno == logging.ERROR: | |
| error.append(record.message) | |
| return debug, info, warning, error | |
| def test_logging_serial_nodes_at_info_level(caplog): | |
| """Test logging of serial nodes at INFO level - log order matters for this test.""" | |
| name = "test_logging_serial_nodes_at_info_level" | |
| caplog.set_level(logging.INFO, logger=name) | |
| def a() -> str: | |
| return "a" | |
| def b(a: str) -> str: | |
| logger = get_logger(name) | |
| logger.warning("Context aware message") | |
| return a + " b" | |
| def c(b: str) -> str: | |
| return b + " c" | |
| modules = ad_hoc_utils.create_temporary_module(a, b, c) | |
| dr = driver.Builder().with_modules(modules).with_adapters(LoggingAdapter(name)).build() | |
| result = dr.execute(["c"]) | |
| assert result["c"] == "a b c" | |
| messages = [record.message for record in caplog.records if record.name == name] | |
| assert messages[0].startswith("Graph run") | |
| assert messages[1:-1] == [ | |
| "Node 'a' - Finished execution [OK]", | |
| "Node 'b' - Context aware message", | |
| "Node 'b' - Finished execution [OK]", | |
| "Node 'c' - Finished execution [OK]", | |
| ] | |
| assert messages[-1].endswith("- Finished graph execution [OK]") | |
| levels = [record.levelname for record in caplog.records if record.name == name] | |
| assert levels == ["INFO", "INFO", "WARNING", "INFO", "INFO", "INFO"] | |
| def test_logging_serial_nodes_at_debug_level(caplog): | |
| """Test logging of serial nodes at DEBUG level - log order matters for this test.""" | |
| name = "test_logging_serial_nodes_at_debug_level" | |
| caplog.set_level(logging.DEBUG, logger=name) | |
| def a() -> str: | |
| return "a" | |
| def b(a: str) -> str: | |
| logger = get_logger(name) | |
| logger.warning("Context aware message") | |
| return a + " b" | |
| def c(b: str) -> str: | |
| return b + " c" | |
| modules = ad_hoc_utils.create_temporary_module(a, b, c) | |
| dr = driver.Builder().with_modules(modules).with_adapters(LoggingAdapter(name)).build() | |
| result = dr.execute(["c"]) | |
| assert result["c"] == "a b c" | |
| messages = [record.message for record in caplog.records if record.name == name] | |
| assert messages[0].startswith("Graph run") | |
| assert messages[1:-1] == [ | |
| "Node 'a' - Starting execution without dependencies", | |
| "Node 'a' - Finished execution [OK]", | |
| "Node 'b' - Starting execution with dependencies 'a'", | |
| "Node 'b' - Context aware message", | |
| "Node 'b' - Finished execution [OK]", | |
| "Node 'c' - Starting execution with dependencies 'b'", | |
| "Node 'c' - Finished execution [OK]", | |
| ] | |
| assert messages[-1].endswith("- Finished graph execution [OK]") | |
| levels = [record.levelname for record in caplog.records if record.name == name] | |
| assert levels == ["INFO", "DEBUG", "INFO", "DEBUG", "WARNING", "INFO", "DEBUG", "INFO", "INFO"] | |
| @pytest.mark.parametrize("adapter", [None, FutureAdapter]) | |
| def test_logging_branching_nodes(caplog, adapter): | |
| """Test logging of branching nodes at multiple logging levels.""" | |
| name = "test_logging_branching_nodes" | |
| caplog.set_level(logging.DEBUG, logger=name) | |
| def a() -> str: | |
| return "a" | |
| def b() -> str: | |
| return "b" | |
| def c() -> str: | |
| logger = get_logger(name) | |
| logger.warning("Context aware message") | |
| return "c" | |
| def d(a: str, b: str) -> str: | |
| return a + " " + b + " d" | |
| def e(c: str) -> str: | |
| return c + " e" | |
| def f(d: str, e: str) -> str: | |
| return d + " " + e + " f" | |
| modules = ad_hoc_utils.create_temporary_module(a, b, c, d, e, f) | |
| adapters = [LoggingAdapter(name)] | |
| if adapter: | |
| adapters.append(adapter()) | |
| dr = driver.Builder().with_modules(modules).with_adapters(*adapters).build() | |
| result = dr.execute(["f"]) | |
| assert result["f"] == "a b d c e f" | |
| debug, info, warning, _ = _split_log_messages(caplog, name) | |
| assert info[0].startswith("Graph run") | |
| assert set(info[1:-1]) == { | |
| "Node 'a' - Finished execution [OK]", | |
| "Node 'b' - Finished execution [OK]", | |
| "Node 'c' - Finished execution [OK]", | |
| "Node 'd' - Finished execution [OK]", | |
| "Node 'e' - Finished execution [OK]", | |
| "Node 'f' - Finished execution [OK]", | |
| } | |
| assert info[-1].endswith("- Finished graph execution [OK]") | |
| assert set(debug) == { | |
| "Node 'a' - Starting execution without dependencies", | |
| "Node 'b' - Starting execution without dependencies", | |
| "Node 'c' - Starting execution without dependencies", | |
| "Node 'd' - Starting execution with dependencies 'a', 'b'", | |
| "Node 'e' - Starting execution with dependencies 'c'", | |
| "Node 'f' - Starting execution with dependencies 'd', 'e'", | |
| } | |
| assert len(warning) == 1 | |
| assert warning[0] == "Node 'c' - Context aware message" | |
| def test_logging_async_nodes(caplog): | |
| """Test logging of async nodes at multiple logging levels.""" | |
| name = "test_logging_async_nodes" | |
| caplog.set_level(logging.DEBUG, logger=name) | |
| async def a() -> str: | |
| return "a" | |
| async def b() -> str: | |
| return "b" | |
| async def c() -> str: | |
| logger = get_logger(name) | |
| logger.warning("Context aware message") | |
| return "c" | |
| async def d(a: str, b: str) -> str: | |
| return a + " " + b + " d" | |
| async def e(c: str) -> str: | |
| return c + " e" | |
| async def f(d: str, e: str) -> str: | |
| return d + " " + e + " f" | |
| async def run_async(module, name): | |
| dr = ( | |
| await async_driver.Builder() # type: ignore | |
| .with_modules(module) | |
| .with_adapters(AsyncLoggingAdapter(name)) | |
| .build() | |
| ) | |
| result = await dr.execute(["f"]) | |
| return result | |
| module = ad_hoc_utils.create_temporary_module(a, b, c, d, e, f) | |
| result = asyncio.run(run_async(module, name)) | |
| assert result["f"] == "a b d c e f" | |
| debug, info, warning, _ = _split_log_messages(caplog, name) | |
| assert info[0].startswith("Graph run") | |
| assert set(info[1:-1]) == { | |
| "Node 'a' - Finished execution [OK]", | |
| "Node 'b' - Finished execution [OK]", | |
| "Node 'c' - Finished execution [OK]", | |
| "Node 'd' - Finished execution [OK]", | |
| "Node 'e' - Finished execution [OK]", | |
| "Node 'f' - Finished execution [OK]", | |
| } | |
| assert info[-1].endswith("- Finished graph execution [OK]") | |
| assert set(debug) == { | |
| "Node 'a' - Submitting async node without dependencies", | |
| "Node 'b' - Submitting async node without dependencies", | |
| "Node 'c' - Submitting async node without dependencies", | |
| "Node 'd' - Submitting async node with dependencies 'a', 'b'", | |
| "Node 'e' - Submitting async node with dependencies 'c'", | |
| "Node 'f' - Submitting async node with dependencies 'd', 'e'", | |
| } | |
| assert len(warning) == 1 | |
| assert warning[0] == "Node 'c' - Context aware message" | |
| @pytest.mark.parametrize( | |
| ("executor_type", "executor_args", "check_context"), | |
| [ | |
| (executors.SynchronousLocalTaskExecutor, {}, True), | |
| (executors.MultiProcessingExecutor, {"max_tasks": 1}, False), | |
| (executors.MultiThreadingExecutor, {"max_tasks": 2}, True), | |
| # (RayTaskExecutor, {}, True), # FIXME: Not available in the CI environment | |
| # (DaskExecutor, {"client": None}, False), # FIXME: Not available in the CI environment | |
| ], | |
| ) | |
| def test_logging_parallel_nodes(caplog, executor_type, executor_args, check_context): | |
| """Test logging of parallel nodes at multiple logging levels.""" | |
| # NOTE: These test is brittle, as it depends on undocumented names of the expanded tasks. | |
| name = "test_logging_parallel_nodes" | |
| caplog.set_level(logging.DEBUG, logger=name) | |
| # FIXME: dask is not available in the CI environment | |
| # if executor_type == DaskExecutor: | |
| # import dask.distributed | |
| # cluster = dask.distributed.LocalCluster(n_workers=5) | |
| # client = dask.distributed.Client(cluster) | |
| # executor_args["client"] = client | |
| adapters = [LoggingAdapter(name)] | |
| dr = ( | |
| driver.Builder() | |
| .with_modules(test_logging_task_nodes) | |
| .with_adapters(*adapters) | |
| .enable_dynamic_execution(allow_experimental_mode=True) | |
| .with_remote_executor(executor_type(**executor_args)) | |
| .build() | |
| ) | |
| result = dr.execute(["f"]) | |
| assert result["f"] == 20 | |
| debug, info, warning, _ = _split_log_messages(caplog, name) | |
| assert info[0].startswith("Graph run") | |
| assert info[1].endswith("task-based logging is enabled") | |
| assert info[-1].endswith("- Finished graph execution [OK]") | |
| assert set(info[2:-1]) == { | |
| "Task 'b' - Task completed [OK]", | |
| "Task 'expand-c' - Task completed [OK]", | |
| "Task 'expand-c.0.block-c' - Task completed [OK]", | |
| "Task 'expand-c.1.block-c' - Task completed [OK]", | |
| "Task 'expand-c.2.block-c' - Task completed [OK]", | |
| "Task 'expand-c.3.block-c' - Task completed [OK]", | |
| "Task 'expand-c.4.block-c' - Task completed [OK]", | |
| "Task 'collect-c' - Task completed [OK]", | |
| "Task 'f' - Task completed [OK]", | |
| } | |
| # Note: Certain executors do not log task and node level debug messages (especially if they | |
| # are not running in the same process as the driver). | |
| local_debug_log = { | |
| "Task 'b' - Initializing new task and submitting to executor", | |
| "Task 'b' - Starting execution", | |
| "Task 'b' - Starting execution without dependencies", | |
| "Task 'b' - Node 'b' - Finished execution [OK]", | |
| "Task 'b' - Finished execution [OK]", | |
| "Task 'expand-c' - Initializing new task and submitting to executor", | |
| "Task 'expand-c' - Starting execution of nodes 'c'", | |
| "Task 'expand-c' - Starting execution with dependencies 'b'", | |
| "Task 'expand-c' - Node 'c' - Finished execution [OK]", | |
| "Task 'expand-c' - Finished execution [OK]", | |
| "Task 'expand-c.0.block-c' - Spawning task and submitting to executor", | |
| "Task 'expand-c.1.block-c' - Spawning task and submitting to executor", | |
| "Task 'expand-c.2.block-c' - Spawning task and submitting to executor", | |
| "Task 'expand-c.3.block-c' - Spawning task and submitting to executor", | |
| "Task 'expand-c.4.block-c' - Spawning task and submitting to executor", | |
| "Task 'collect-c' - Initializing new task and submitting to executor", | |
| "Task 'collect-c' - Starting execution of nodes 'e'", | |
| "Task 'collect-c' - Starting execution with dependencies 'd'", | |
| "Task 'collect-c' - Node 'e' - Finished execution [OK]", | |
| "Task 'collect-c' - Finished execution [OK]", | |
| "Task 'f' - Initializing new task and submitting to executor", | |
| "Task 'f' - Starting execution", | |
| "Task 'f' - Starting execution with dependencies 'e'", | |
| "Task 'f' - Node 'f' - Finished execution [OK]", | |
| "Task 'f' - Finished execution [OK]", | |
| } | |
| assert local_debug_log.difference(set(debug)) == set() | |
| if check_context: | |
| assert set(warning) == { | |
| "Task 'expand-c.0.block-c' - Context aware message", | |
| "Task 'expand-c.1.block-c' - Context aware message", | |
| "Task 'expand-c.2.block-c' - Context aware message", | |
| "Task 'expand-c.3.block-c' - Context aware message", | |
| "Task 'expand-c.4.block-c' - Context aware message", | |
| } | |
| def test_logging_with_inputs(caplog): | |
| """Test logging of nodes with inputs.""" | |
| name = "test_logging_with_inputs" | |
| caplog.set_level(logging.DEBUG, logger=name) | |
| def a(x: str) -> str: | |
| return x | |
| modules = ad_hoc_utils.create_temporary_module(a) | |
| dr = driver.Builder().with_modules(modules).with_adapters(LoggingAdapter(name)).build() | |
| result = dr.execute(["a"], inputs={"x": "test"}) | |
| assert result["a"] == "test" | |
| _, info, _, _ = _split_log_messages(caplog, name) | |
| assert info[1].endswith("Using inputs 'x'") | |
| def test_logging_with_overrides(caplog): | |
| """Test logging of nodes with overrides.""" | |
| name = "test_logging_with_overrides" | |
| caplog.set_level(logging.DEBUG, logger=name) | |
| def a(x: str) -> str: | |
| return x | |
| modules = ad_hoc_utils.create_temporary_module(a) | |
| dr = driver.Builder().with_modules(modules).with_adapters(LoggingAdapter(name)).build() | |
| result = dr.execute(["a"], overrides={"a": "test"}) | |
| assert result["a"] == "test" | |
| _, info, _, _ = _split_log_messages(caplog, name) | |
| assert info[1].endswith("Using overrides 'a'") |