Skip to content

Commit 1b1c133

Browse files
Feature:4005 Docker settings usage warnings
- Warn when used without containerized orchestrator in place
1 parent 8e14213 commit 1b1c133

File tree

8 files changed

+557
-4
lines changed

8 files changed

+557
-4
lines changed

src/zenml/config/compiler.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from zenml import __version__
3030
from zenml.config.base_settings import BaseSettings, ConfigurationLevel
31+
from zenml.config.constants import DOCKER_SETTINGS_KEY
3132
from zenml.config.pipeline_configurations import PipelineConfiguration
3233
from zenml.config.pipeline_run_configuration import PipelineRunConfiguration
3334
from zenml.config.pipeline_spec import OutputSpec, PipelineSpec
@@ -43,6 +44,7 @@
4344
from zenml.models import PipelineSnapshotBase
4445
from zenml.pipelines.run_utils import get_default_run_name
4546
from zenml.utils import pydantic_utils, secret_utils, settings_utils
47+
from zenml.utils.warnings import WARNING_CONTROLLER, WarningCodes
4648

4749
if TYPE_CHECKING:
4850
from zenml.pipelines.pipeline_definition import Pipeline
@@ -369,6 +371,46 @@ def _verify_upstream_steps(
369371
f"steps in this pipeline: {available_steps}."
370372
)
371373

374+
@staticmethod
375+
def _validate_docker_settings_usage(
376+
docker_settings: "BaseSettings | None",
377+
stack: "Stack",
378+
) -> None:
379+
"""Validates that docker settings are used with a proper stack.
380+
381+
Generates warning for improper docker settings usage or returns.
382+
383+
Args:
384+
docker_settings: The docker settings specified for the step/pipeline.
385+
stack: The stack the settings are validated against.
386+
387+
"""
388+
from zenml.orchestrators import (
389+
ContainerizedOrchestrator,
390+
LocalOrchestrator,
391+
)
392+
393+
if not docker_settings or isinstance(
394+
stack.orchestrator, ContainerizedOrchestrator
395+
):
396+
return
397+
398+
warning_message = (
399+
"You are specifying docker settings but you are not using a"
400+
f"containerized orchestrator: {stack.orchestrator.__class__.__name__}."
401+
f"Consider switching stack or using a containerized orchestrator, otherwise"
402+
f"your docker settings will be ignored."
403+
)
404+
405+
if isinstance(stack.orchestrator, LocalOrchestrator):
406+
WARNING_CONTROLLER.info(
407+
warning_code=WarningCodes.ZML002, message=warning_message
408+
)
409+
else:
410+
WARNING_CONTROLLER.warn(
411+
warning_code=WarningCodes.ZML002, message=warning_message
412+
)
413+
372414
def _filter_and_validate_settings(
373415
self,
374416
settings: Dict[str, "BaseSettings"],
@@ -397,9 +439,10 @@ def _filter_and_validate_settings(
397439
try:
398440
settings_instance = resolver.resolve(stack=stack)
399441
except KeyError:
400-
logger.info(
401-
"Not including stack component settings with key `%s`.",
402-
key,
442+
WARNING_CONTROLLER.info(
443+
warning_code=WarningCodes.ZML001.value,
444+
message="Not including stack component settings with key {key}",
445+
key=key,
403446
)
404447
continue
405448

@@ -429,6 +472,11 @@ def _filter_and_validate_settings(
429472

430473
validated_settings[key] = settings_instance
431474

475+
self._validate_docker_settings_usage(
476+
stack=stack,
477+
docker_settings=settings.get(DOCKER_SETTINGS_KEY),
478+
)
479+
432480
return validated_settings
433481

434482
def _get_step_spec(

src/zenml/logger.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,9 @@ def init_logging() -> None:
359359
for handler in root_logger.handlers
360360
)
361361

362+
# logging capture warnings
363+
logging.captureWarnings(True)
364+
362365
if not has_console_handler:
363366
console_handler = logging.StreamHandler(sys.stdout)
364367
console_handler.setFormatter(get_formatter())
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) ZenML GmbH 2025. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at:
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
# or implied. See the License for the specific language governing
13+
# permissions and limitations under the License.
14+
"""Initialization of the warning utils module.
15+
16+
The `warnings` module contains utilities that help centralize
17+
and improve warning management.
18+
"""
19+
20+
from zenml.utils.warnings.controller import WarningController
21+
from zenml.utils.warnings.registry import WarningCodes
22+
23+
WARNING_CONTROLLER = WarningController.create()
24+
25+
__all__ = [
26+
"WARNING_CONTROLLER",
27+
"WarningCodes"
28+
]

src/zenml/utils/warnings/base.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (c) ZenML GmbH 2025. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at:
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
# or implied. See the License for the specific language governing
13+
# permissions and limitations under the License.
14+
"""Warning configuration class and helper enums."""
15+
16+
from enum import Enum
17+
18+
from pydantic import BaseModel, Field
19+
20+
21+
class WarningSeverity(str, Enum):
22+
"""Enum class describing the warning severity."""
23+
24+
LOW = "low"
25+
MEDIUM = "medium"
26+
HIGH = "high"
27+
28+
29+
class WarningCategory(str, Enum):
30+
"""Enum class describing the warning category."""
31+
32+
USAGE = "USAGE"
33+
34+
35+
class WarningVerbosity(str, Enum):
36+
"""Enum class describing the warning verbosity."""
37+
38+
LOW = "low"
39+
MEDIUM = "medium"
40+
HIGH = "high"
41+
42+
43+
class WarningConfig(BaseModel, use_enum_values=True):
44+
"""Warning config class describing how warning messages should be displayed."""
45+
46+
description: str = Field(description="Description of the warning message")
47+
severity: WarningSeverity = WarningSeverity.MEDIUM
48+
category: WarningCategory
49+
code: str
50+
is_throttled: bool = Field(
51+
description="If the warning is throttled. Throttled warnings should be displayed once per session.",
52+
default=False,
53+
)
54+
verbosity: WarningVerbosity = Field(
55+
description="Verbosity of the warning. Low verbosity displays basic details (code & message). Medium displays also call details like module & line number. High displays description and additional info.",
56+
default=WarningVerbosity.MEDIUM,
57+
)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Copyright (c) ZenML GmbH 2025. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at:
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
# or implied. See the License for the specific language governing
13+
# permissions and limitations under the License.
14+
"""Module for centralized WarningController implementation."""
15+
16+
import logging
17+
from collections import defaultdict
18+
from typing import Any
19+
20+
from zenml.enums import LoggingLevels
21+
from zenml.utils.singleton import SingletonMetaClass
22+
from zenml.utils.warnings.base import WarningConfig, WarningVerbosity
23+
24+
logger = logging.getLogger(__name__)
25+
26+
27+
class WarningController(metaclass=SingletonMetaClass):
28+
"""Class responsible for centralized handling of warning messages."""
29+
30+
def __init__(self) -> None:
31+
"""WarningController constructor."""
32+
self._warning_configs: dict[str, WarningConfig] = {}
33+
self._warning_statistics: dict[str, int] = defaultdict(int)
34+
35+
def register(self, warning_configs: dict[str, WarningConfig]) -> None:
36+
"""Register a warning config collection to the controller.
37+
38+
Args:
39+
warning_configs: Configs to be registered. Key should be the warning code.
40+
"""
41+
self._warning_configs.update(warning_configs)
42+
43+
@staticmethod
44+
def _resolve_call_details() -> tuple[str, int]:
45+
import inspect
46+
47+
frame = inspect.stack()[3] # public methods -> _log -> _resolve
48+
module = inspect.getmodule(frame[0])
49+
module_name = module.__name__ if module else "<unknown module>"
50+
line_number = frame.lineno
51+
52+
return module_name, line_number
53+
54+
@staticmethod
55+
def _get_display_message(
56+
message: str,
57+
module_name: str,
58+
line_number: int,
59+
config: WarningConfig,
60+
) -> str:
61+
"""Helper method to build the warning message string.
62+
63+
Args:
64+
message: The warning message.
65+
module_name: The module that the warning call originated from.
66+
line_number: The line number that the warning call originated from.
67+
config: The warning configuration.
68+
69+
Returns:
70+
A warning message containing extra fields/info based on warning config.
71+
"""
72+
display = f"[{config.code}]({config.category}) - {message}"
73+
74+
if config.verbosity == WarningVerbosity.MEDIUM:
75+
display = f"{module_name}:{line_number} {display}"
76+
77+
if config.verbosity == WarningVerbosity.HIGH:
78+
display = f"{display}\n{config.description}"
79+
80+
return display
81+
82+
def _log(
83+
self,
84+
warning_code: str,
85+
message: str,
86+
level: LoggingLevels,
87+
**kwargs: dict[str, Any],
88+
) -> None:
89+
"""Core function for warning handling.
90+
91+
Args:
92+
warning_code: The code of the warning configuration.
93+
message: The warning message.
94+
level: The level of the warning.
95+
**kwargs: Keyword arguments (for formatted messages).
96+
97+
"""
98+
warning_config = self._warning_configs.get(warning_code)
99+
100+
# resolves the module and line number of the warning call.
101+
module_name, line_number = self._resolve_call_details()
102+
103+
if not warning_config:
104+
# If no config is available just follow default behavior:
105+
logger.warning(f"{module_name}:{line_number} - {message}")
106+
return
107+
108+
if warning_config.is_throttled:
109+
if warning_code in self._warning_statistics:
110+
# Throttled code has already appeared - skip.
111+
return
112+
113+
display_message = self._get_display_message(
114+
message=message,
115+
module_name=module_name,
116+
line_number=line_number,
117+
config=warning_config,
118+
)
119+
120+
self._warning_statistics[warning_code] += 1
121+
122+
if level == LoggingLevels.INFO:
123+
logger.info(display_message.format(**kwargs))
124+
else:
125+
# Assumes warning level is the default if an invalid option is passed.
126+
logger.warning(display_message.format(**kwargs))
127+
128+
def warn(self, *, warning_code: str, message: str, **kwargs: Any) -> None:
129+
"""Method to execute warning handling logic with warning log level.
130+
131+
Args:
132+
warning_code: The code of the warning (see WarningCodes enum)
133+
message: The message to display.
134+
**kwargs: Keyword arguments (for formatted messages).
135+
"""
136+
self._log(warning_code, message, LoggingLevels.WARNING, **kwargs)
137+
138+
def info(self, *, warning_code: str, message: str, **kwargs: Any) -> None:
139+
"""Method to execute warning handling logic with info log level.
140+
141+
Args:
142+
warning_code: The code of the warning (see WarningCodes enum)
143+
message: The message to display.
144+
**kwargs: Keyword arguments (for formatted messages).
145+
"""
146+
self._log(warning_code, message, LoggingLevels.INFO, **kwargs)
147+
148+
@staticmethod
149+
def create() -> "WarningController":
150+
"""Factory function for WarningController.
151+
152+
Creates a new warning controller and registers system warning configs.
153+
154+
Returns:
155+
A warning controller instance.
156+
"""
157+
from zenml.utils.warnings.registry import WARNING_CONFIG_REGISTRY
158+
159+
registry = WarningController()
160+
161+
registry.register(warning_configs=WARNING_CONFIG_REGISTRY)
162+
163+
return registry

0 commit comments

Comments
 (0)