py-wlcli 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_wlcli-0.8.0.dist-info/METADATA +139 -0
- py_wlcli-0.8.0.dist-info/RECORD +320 -0
- py_wlcli-0.8.0.dist-info/WHEEL +4 -0
- py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
- py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
- wl_cli/__init__.py +27 -0
- wl_cli/__main__.py +130 -0
- wl_cli/capabilities.py +79 -0
- wl_cli/cc/__init__.py +5 -0
- wl_cli/cc/checks.py +1019 -0
- wl_cli/cc/cli.py +370 -0
- wl_cli/cc/mutate.py +1350 -0
- wl_cli/cli_click.py +156 -0
- wl_cli/commands/__init__.py +84 -0
- wl_cli/commands/base.py +80 -0
- wl_cli/commands/buildcommands/__init__.py +1 -0
- wl_cli/commands/buildcommands/build.py +71 -0
- wl_cli/commands/buildcommands/build_utils.py +47 -0
- wl_cli/commands/buildcommands/builders/__init__.py +18 -0
- wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
- wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
- wl_cli/commands/buildcommands/builders/environment.py +81 -0
- wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
- wl_cli/commands/buildcommands/builders/stubs.py +58 -0
- wl_cli/commands/buildcommands/dist.py +326 -0
- wl_cli/commands/buildcommands/test.py +89 -0
- wl_cli/commands/clean/__init__.py +1 -0
- wl_cli/commands/clean/build.py +151 -0
- wl_cli/commands/clean/clean.py +294 -0
- wl_cli/commands/clean/clean_utils.py +77 -0
- wl_cli/commands/clean/core.py +236 -0
- wl_cli/commands/clean/dead_code/__init__.py +0 -0
- wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
- wl_cli/commands/clean/dead_code/detector.py +161 -0
- wl_cli/commands/clean/dead_code/fixer.py +176 -0
- wl_cli/commands/clean/dead_code/reporter.py +295 -0
- wl_cli/commands/clean/dead_code/utils.py +0 -0
- wl_cli/commands/clean/lfs.py +215 -0
- wl_cli/commands/clean/python.py +81 -0
- wl_cli/commands/clean/rust.py +91 -0
- wl_cli/commands/clean/utils/__init__.py +34 -0
- wl_cli/commands/clean/venv.py +91 -0
- wl_cli/commands/cleanup.py +73 -0
- wl_cli/commands/click_base.py +79 -0
- wl_cli/commands/config/__init__.py +1 -0
- wl_cli/commands/config/config.py +291 -0
- wl_cli/commands/debug/__init__.py +6 -0
- wl_cli/commands/debug/debug.py +309 -0
- wl_cli/commands/debug/debug_cache.py +86 -0
- wl_cli/commands/debug/debug_subagent.py +156 -0
- wl_cli/commands/debug/output_formatter.py +165 -0
- wl_cli/commands/debug/source_hasher.py +92 -0
- wl_cli/commands/format/__init__.py +13 -0
- wl_cli/commands/format/format_cache_adapter.py +125 -0
- wl_cli/commands/format/format_command.py +197 -0
- wl_cli/commands/format/format_coordinator.py +191 -0
- wl_cli/commands/format/format_executor.py +470 -0
- wl_cli/commands/format/format_outcome.py +44 -0
- wl_cli/commands/format/format_path_handler.py +1016 -0
- wl_cli/commands/format/format_progress.py +62 -0
- wl_cli/commands/format/format_reporter.py +31 -0
- wl_cli/commands/format/python_formatter.py +506 -0
- wl_cli/commands/format/rust_formatter.py +106 -0
- wl_cli/commands/gate/__init__.py +1 -0
- wl_cli/commands/gate/gate_command.py +862 -0
- wl_cli/commands/gate/preflight.py +445 -0
- wl_cli/commands/git/__init__.py +1 -0
- wl_cli/commands/git/config.py +169 -0
- wl_cli/commands/git/git_facts.py +60 -0
- wl_cli/commands/git/git_flow.py +86 -0
- wl_cli/commands/git/hook.py +112 -0
- wl_cli/commands/git/policy.py +216 -0
- wl_cli/commands/git/push_update.py +82 -0
- wl_cli/commands/initenv/__init__.py +1 -0
- wl_cli/commands/initenv/common/__init__.py +23 -0
- wl_cli/commands/initenv/common/check_git_bash.py +78 -0
- wl_cli/commands/initenv/common/error_handler.py +68 -0
- wl_cli/commands/initenv/common/exceptions.py +52 -0
- wl_cli/commands/initenv/components/__init__.py +4 -0
- wl_cli/commands/initenv/components/cargo_sync.py +240 -0
- wl_cli/commands/initenv/components/config_manager.py +106 -0
- wl_cli/commands/initenv/components/directory_utils.py +71 -0
- wl_cli/commands/initenv/components/git/__init__.py +9 -0
- wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
- wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
- wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
- wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
- wl_cli/commands/initenv/components/git/git_utility.py +254 -0
- wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
- wl_cli/commands/initenv/components/git_initializer.py +212 -0
- wl_cli/commands/initenv/components/i18n_manager.py +104 -0
- wl_cli/commands/initenv/components/log_manager.py +241 -0
- wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
- wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
- wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
- wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
- wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
- wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
- wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
- wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
- wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
- wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
- wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
- wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
- wl_cli/commands/initenv/components/rust_initializer.py +145 -0
- wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
- wl_cli/commands/initenv/components/symlink_manager.py +52 -0
- wl_cli/commands/initenv/components/venv_manager.py +118 -0
- wl_cli/commands/initenv/initenv.py +169 -0
- wl_cli/commands/initenv/services/__init__.py +11 -0
- wl_cli/commands/initenv/services/environment_initializer.py +288 -0
- wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
- wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
- wl_cli/commands/lfs/README.md +221 -0
- wl_cli/commands/lfs/__init__.py +5 -0
- wl_cli/commands/lfs/cli.py +106 -0
- wl_cli/commands/lfs/config.py +164 -0
- wl_cli/commands/lfs/hook.py +61 -0
- wl_cli/commands/lfs/initializer.py +531 -0
- wl_cli/commands/lfs/manager.py +95 -0
- wl_cli/commands/lfs/policy.py +165 -0
- wl_cli/commands/lfs/watcher.py +294 -0
- wl_cli/commands/lint/__init__.py +1 -0
- wl_cli/commands/lint/lint_command.py +297 -0
- wl_cli/commands/lint/lint_executor.py +409 -0
- wl_cli/commands/lint/lint_reporter.py +579 -0
- wl_cli/commands/lint/lint_run.py +258 -0
- wl_cli/commands/lint/mypy_config.py +90 -0
- wl_cli/commands/registry.py +103 -0
- wl_cli/commands/self/__init__.py +7 -0
- wl_cli/commands/self/common/__init__.py +8 -0
- wl_cli/commands/self/common/capabilities.py +30 -0
- wl_cli/commands/self/common/config.py +27 -0
- wl_cli/commands/self/common/version_cache.py +85 -0
- wl_cli/commands/self/handlers/__init__.py +19 -0
- wl_cli/commands/self/handlers/base_handler.py +28 -0
- wl_cli/commands/self/handlers/install_handler.py +85 -0
- wl_cli/commands/self/handlers/status_handler.py +66 -0
- wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
- wl_cli/commands/self/handlers/update_handler.py +102 -0
- wl_cli/commands/self/handlers/version_handler.py +16 -0
- wl_cli/commands/self/self_command.py +237 -0
- wl_cli/commands/self/services/__init__.py +15 -0
- wl_cli/commands/self/services/installation_manager.py +187 -0
- wl_cli/commands/self/services/local_installer.py +165 -0
- wl_cli/commands/self/services/pypi_updater.py +278 -0
- wl_cli/commands/self/services/windows_update.py +174 -0
- wl_cli/commands/testcommands/__init__.py +8 -0
- wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
- wl_cli/commands/testcommands/batch_processor.py +278 -0
- wl_cli/commands/testcommands/cache_manager.py +1156 -0
- wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
- wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
- wl_cli/commands/testcommands/progress_manager.py +79 -0
- wl_cli/commands/testcommands/python_env_detector.py +207 -0
- wl_cli/commands/testcommands/report_generator.py +101 -0
- wl_cli/commands/testcommands/single_test_executor.py +250 -0
- wl_cli/commands/testcommands/speed_checker.py +118 -0
- wl_cli/commands/testcommands/test_command_builder.py +165 -0
- wl_cli/commands/testcommands/test_commands.py +786 -0
- wl_cli/commands/testcommands/test_reporter.py +181 -0
- wl_cli/commands/testcommands/test_result_handler.py +96 -0
- wl_cli/commands/testcommands/test_runner.py +1343 -0
- wl_cli/commands/testcommands/test_utils.py +197 -0
- wl_cli/commands/testcommands/utils/__init__.py +17 -0
- wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
- wl_cli/commands/testcommands/utils/env_info.py +36 -0
- wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
- wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
- wl_cli/commands/testcommands/utils/project_root.py +61 -0
- wl_cli/commands/update/__init__.py +9 -0
- wl_cli/commands/update/services/__init__.py +21 -0
- wl_cli/commands/update/services/backup_manager.py +207 -0
- wl_cli/commands/update/services/component_manager.py +83 -0
- wl_cli/commands/update/services/conflict_resolver.py +189 -0
- wl_cli/commands/update/services/vendors_updater.py +85 -0
- wl_cli/commands/update/services/version_tracker.py +127 -0
- wl_cli/commands/update/update_command.py +270 -0
- wl_cli/commands/update/utils/__init__.py +3 -0
- wl_cli/commands/version/__init__.py +9 -0
- wl_cli/commands/version/version_command.py +155 -0
- wl_cli/commands/workflow.py +477 -0
- wl_cli/env/__init__.py +5 -0
- wl_cli/env/cli.py +167 -0
- wl_cli/env_status.py +157 -0
- wl_cli/envelope.py +91 -0
- wl_cli/exceptions.py +85 -0
- wl_cli/lib/__init__.py +3 -0
- wl_cli/lib/rust_utils.py +35 -0
- wl_cli/mcp/__init__.py +6 -0
- wl_cli/mcp/capabilities.py +19 -0
- wl_cli/mcp/cli.py +22 -0
- wl_cli/mcp/gitlab.py +553 -0
- wl_cli/mcp/k8s.py +344 -0
- wl_cli/mcp/protocol.py +159 -0
- wl_cli/platform.py +139 -0
- wl_cli/quality/__init__.py +18 -0
- wl_cli/utils/__init__.py +7 -0
- wl_cli/utils/cache.py +491 -0
- wl_cli/utils/cache_ledger.py +355 -0
- wl_cli/utils/cache_store.py +546 -0
- wl_cli/utils/config/__init__.py +27 -0
- wl_cli/utils/config/config_loader.py +163 -0
- wl_cli/utils/config/core.py +862 -0
- wl_cli/utils/config/defaults.py +142 -0
- wl_cli/utils/config/env_exporter.py +86 -0
- wl_cli/utils/config/env_parser.py +108 -0
- wl_cli/utils/config/validation.py +280 -0
- wl_cli/utils/error_handler.py +165 -0
- wl_cli/utils/file_copier.py +56 -0
- wl_cli/utils/file_operations.py +506 -0
- wl_cli/utils/language_utils.py +122 -0
- wl_cli/utils/lazy_import.py +240 -0
- wl_cli/utils/log_levels.py +35 -0
- wl_cli/utils/log_rotators.py +74 -0
- wl_cli/utils/logging.py +458 -0
- wl_cli/utils/output_context.py +142 -0
- wl_cli/utils/performance/__init__.py +1 -0
- wl_cli/utils/performance/decorators.py +111 -0
- wl_cli/utils/performance/exporter.py +38 -0
- wl_cli/utils/performance/models.py +16 -0
- wl_cli/utils/performance/monitor.py +161 -0
- wl_cli/utils/performance_monitor.py +32 -0
- wl_cli/utils/platform_adapter.py +131 -0
- wl_cli/utils/pre_commit.py +266 -0
- wl_cli/utils/pre_commit_config.py +72 -0
- wl_cli/utils/root.py +521 -0
- wl_cli/utils/root_fallback.py +256 -0
- wl_cli/utils/run_evidence.py +236 -0
- wl_cli/utils/structured_logger.py +18 -0
- wl_cli/utils/structured_logging/__init__.py +19 -0
- wl_cli/utils/structured_logging/config.py +337 -0
- wl_cli/utils/structured_logging/core.py +177 -0
- wl_cli/utils/structured_logging/env.py +56 -0
- wl_cli/utils/structured_logging/filtering.py +75 -0
- wl_cli/utils/structured_logging/filters.py +55 -0
- wl_cli/utils/structured_logging/formatters.py +251 -0
- wl_cli/utils/structured_logging/handlers.py +308 -0
- wl_cli/utils/structured_logging/logger.py +354 -0
- wl_cli/utils/subprocess.py +71 -0
- wl_cli/utils/subprocess_utils.py +260 -0
- wl_cli/utils/test_utils/__init__.py +5 -0
- wl_cli/utils/test_utils/test_filter.py +36 -0
- wl_cli/utils/uv_run.py +79 -0
- wl_cli/utils/uv_tool.py +117 -0
- wl_cli/utils/uv_tree.py +71 -0
- wl_cli/utils/uv_workspace.py +174 -0
- wl_cli/utils/version/__init__.py +15 -0
- wl_cli/utils/version/pypi_version_checker.py +95 -0
- wl_cli/utils/version/version_comparator.py +133 -0
- wl_cli/utils/version/version_detectors.py +312 -0
- wl_cli/utils/version/version_service.py +131 -0
- wl_cli/utils/version/version_updater.py +198 -0
- wl_cli/utils/wl_dir_updater.py +1440 -0
- wl_cli/utils/workspace_detector/__init__.py +28 -0
- wl_cli/utils/workspace_detector/core.py +161 -0
- wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
- wl_cli/utils/workspace_detector/exceptions.py +5 -0
- wl_cli/utils/workspace_detector/types.py +17 -0
- wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
- wl_cli/vendors/config/.codespellrc +2 -0
- wl_cli/vendors/config/dependencies.toml +10 -0
- wl_cli/vendors/config/mypy.ini +62 -0
- wl_cli/vendors/config/pyproject_defaults.toml +28 -0
- wl_cli/vendors/config/template_config.toml +24 -0
- wl_cli/vendors/config/tool_configs.toml +76 -0
- wl_cli/vendors/git/.gitignore +122 -0
- wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
- wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/post-update.sample +8 -0
- wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/pre-commit +45 -0
- wl_cli/vendors/hooks/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/pre-push.sample +60 -0
- wl_cli/vendors/hooks/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
- wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
- wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
- wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
- wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/unix/post-update.sample +8 -0
- wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/unix/pre-commit +190 -0
- wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
- wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/unix/pre-push +417 -0
- wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
- wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/update.sample +128 -0
- wl_cli/vendors/hooks/win/work.bat +63 -0
- wl_cli/vendors/project/.gitignore +90 -0
- wl_cli/vendors/project/.python-version +1 -0
- wl_cli/vendors/project/LICENSE +21 -0
- wl_cli/vendors/project/README.md +0 -0
- wl_cli/vendors/project/pyproject.toml +27 -0
- wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
- wl_cli/vendors/project/pyproject_workspace.toml +14 -0
- wl_cli/vendors/readme/README.md +120 -0
- wl_cli/vendors/rust/Cargo.toml +14 -0
- wl_cli/vendors/rust/lib/__init__.py +3 -0
- wl_cli/vendors/rust/lib/rust_utils.py +31 -0
- wl_cli/vendors/rust/src/lib.rs +13 -0
- wl_cli/vendors/rust/src/main.rs +3 -0
- wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
- wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
- wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
- wl_cli/vendors/sop_doc/python-sop.md +342 -0
- wl_cli/vendors/sop_doc/rust-readme.md +219 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Configuration management for structured logging."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from ..log_levels import LogLevel
|
|
7
|
+
from .env import is_test_environment
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoggingConfig:
|
|
11
|
+
"""Manages configuration for structured logging."""
|
|
12
|
+
|
|
13
|
+
# Singleton instance for consistent configuration across the application
|
|
14
|
+
_instance = None
|
|
15
|
+
|
|
16
|
+
def __new__(cls, config_dict: dict[str, Any] | None = None) -> "LoggingConfig":
|
|
17
|
+
"""Create or return the singleton instance."""
|
|
18
|
+
if cls._instance is None:
|
|
19
|
+
cls._instance = super().__new__(cls)
|
|
20
|
+
# Initialize only once
|
|
21
|
+
cls._instance._initialize(config_dict)
|
|
22
|
+
elif config_dict:
|
|
23
|
+
# Update existing instance with new config if provided
|
|
24
|
+
cls._instance.update(config_dict)
|
|
25
|
+
return cls._instance
|
|
26
|
+
|
|
27
|
+
def _initialize(self, config_dict: dict[str, Any] | None = None) -> None:
|
|
28
|
+
"""Initialize logging configuration.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config_dict: Optional dictionary with initial configuration.
|
|
32
|
+
"""
|
|
33
|
+
self._config_functions = self._get_config_functions()
|
|
34
|
+
self._explicit_config = config_dict or {}
|
|
35
|
+
# Add configuration caching for better performance
|
|
36
|
+
self._cache: dict[str, Any] = {}
|
|
37
|
+
# Disable caching in test environment to prevent stale cache from mocks.
|
|
38
|
+
# is_test_environment covers plain pytest, xdist workers, and wl test's
|
|
39
|
+
# NON_INTERACTIVE escape hatch.
|
|
40
|
+
self._cache_enabled = not is_test_environment()
|
|
41
|
+
|
|
42
|
+
def _get_config_functions(self):
|
|
43
|
+
"""Get configuration functions with fallbacks."""
|
|
44
|
+
try:
|
|
45
|
+
from ..config import get_config
|
|
46
|
+
|
|
47
|
+
return get_config
|
|
48
|
+
except ImportError:
|
|
49
|
+
# Fallback if config is not available
|
|
50
|
+
def config(key: str, default: Any = None) -> Any:
|
|
51
|
+
return default
|
|
52
|
+
|
|
53
|
+
return config
|
|
54
|
+
|
|
55
|
+
def _get_environment(self) -> str:
|
|
56
|
+
"""Get current environment."""
|
|
57
|
+
try:
|
|
58
|
+
from ..config import get_config
|
|
59
|
+
|
|
60
|
+
return cast("str", get_config("environment", "development"))
|
|
61
|
+
except ImportError:
|
|
62
|
+
# Fallback to environment variable
|
|
63
|
+
return os.environ.get("WL_ENVIRONMENT", "development")
|
|
64
|
+
|
|
65
|
+
def get_environment_specific_config(self, key: str, default: Any = None) -> Any:
|
|
66
|
+
"""Get environment-specific configuration."""
|
|
67
|
+
environment = self._get_environment()
|
|
68
|
+
|
|
69
|
+
# Try environment-specific key first
|
|
70
|
+
env_specific_key = f"{key}_{environment}"
|
|
71
|
+
value = self._config_functions(env_specific_key)
|
|
72
|
+
if value is not None:
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
# Fallback to regular key
|
|
76
|
+
return self._config_functions(key, default)
|
|
77
|
+
|
|
78
|
+
def get_log_level(self, min_level: int | None = None) -> int:
|
|
79
|
+
"""
|
|
80
|
+
Get log level from config or parameter.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
min_level: Explicit log level. If None, get from config.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Log level as integer.
|
|
87
|
+
"""
|
|
88
|
+
if min_level is not None:
|
|
89
|
+
return min_level
|
|
90
|
+
|
|
91
|
+
# Never cache in test environment to prevent stale values from mocks.
|
|
92
|
+
# is_test_environment covers plain pytest, xdist workers, and wl test's
|
|
93
|
+
# NON_INTERACTIVE escape hatch.
|
|
94
|
+
if is_test_environment():
|
|
95
|
+
cache_enabled = False
|
|
96
|
+
# Clear any existing cached values from previous tests
|
|
97
|
+
self._cache.clear()
|
|
98
|
+
else:
|
|
99
|
+
cache_enabled = self._cache_enabled
|
|
100
|
+
|
|
101
|
+
# Check cache first
|
|
102
|
+
cache_key = f"log_level_{self._get_environment()}"
|
|
103
|
+
if cache_enabled and cache_key in self._cache:
|
|
104
|
+
return cast("int", self._cache[cache_key])
|
|
105
|
+
|
|
106
|
+
# Check explicit config first
|
|
107
|
+
if "log_level" in self._explicit_config:
|
|
108
|
+
level_value = self._explicit_config["log_level"]
|
|
109
|
+
else:
|
|
110
|
+
# Get environment-specific log level
|
|
111
|
+
level_value = self.get_environment_specific_config("log_level", "INFO")
|
|
112
|
+
|
|
113
|
+
# Handle both string and numeric log levels
|
|
114
|
+
if isinstance(level_value, str):
|
|
115
|
+
level_name = level_value.upper()
|
|
116
|
+
log_level = getattr(LogLevel, level_name, LogLevel.INFO)
|
|
117
|
+
else:
|
|
118
|
+
log_level = level_value
|
|
119
|
+
|
|
120
|
+
# Cache the result
|
|
121
|
+
if cache_enabled:
|
|
122
|
+
self._cache[cache_key] = log_level
|
|
123
|
+
|
|
124
|
+
return log_level
|
|
125
|
+
|
|
126
|
+
def get_console_settings(self) -> tuple[bool, str]:
|
|
127
|
+
"""
|
|
128
|
+
Get console output settings.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Tuple of (enable_console, console_format).
|
|
132
|
+
"""
|
|
133
|
+
# Check explicit config first
|
|
134
|
+
if "log_console" in self._explicit_config:
|
|
135
|
+
enable_console = self._explicit_config["log_console"]
|
|
136
|
+
else:
|
|
137
|
+
# Get environment-specific console setting
|
|
138
|
+
enable_console = self.get_environment_specific_config("log_console", False)
|
|
139
|
+
|
|
140
|
+
if "log_console_format" in self._explicit_config:
|
|
141
|
+
console_format = self._explicit_config["log_console_format"]
|
|
142
|
+
else:
|
|
143
|
+
# 首先尝试从环境变量获取
|
|
144
|
+
env_value = os.environ.get("WL_LOG_CONSOLE_FORMAT")
|
|
145
|
+
if env_value:
|
|
146
|
+
console_format = env_value
|
|
147
|
+
else:
|
|
148
|
+
# Get environment-specific console format
|
|
149
|
+
console_format = self.get_environment_specific_config(
|
|
150
|
+
"log_console_format", "colored"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return enable_console, console_format
|
|
154
|
+
|
|
155
|
+
def get_file_format_settings(self) -> str:
|
|
156
|
+
"""
|
|
157
|
+
Get log file format setting.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Log file format ('human' or 'json').
|
|
161
|
+
"""
|
|
162
|
+
# Check explicit config first
|
|
163
|
+
if "log_file_format" in self._explicit_config:
|
|
164
|
+
return cast("str", self._explicit_config["log_file_format"])
|
|
165
|
+
else:
|
|
166
|
+
# Get environment-specific file format
|
|
167
|
+
return cast(
|
|
168
|
+
"str", self.get_environment_specific_config("log_file_format", "human")
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def get_log_file(self, log_file: str | None = None) -> str | None:
|
|
172
|
+
"""
|
|
173
|
+
Get log file path.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
log_file: Explicit log file path. If None, get from config.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Log file path or None.
|
|
180
|
+
"""
|
|
181
|
+
if log_file is not None:
|
|
182
|
+
return log_file
|
|
183
|
+
|
|
184
|
+
# Check explicit config first
|
|
185
|
+
if "log_file" in self._explicit_config:
|
|
186
|
+
return cast("str | None", self._explicit_config["log_file"])
|
|
187
|
+
else:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
def get_rotation_settings(self) -> dict[str, Any]:
|
|
191
|
+
"""
|
|
192
|
+
Get log rotation settings.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Dictionary with rotation settings.
|
|
196
|
+
"""
|
|
197
|
+
# Check explicit config first
|
|
198
|
+
if "rotation" in self._explicit_config:
|
|
199
|
+
return cast("dict[str, Any]", self._explicit_config["rotation"])
|
|
200
|
+
else:
|
|
201
|
+
return {
|
|
202
|
+
"max_size": self._config_functions("log_max_size", 10 * 1024 * 1024),
|
|
203
|
+
"max_backups": self._config_functions("log_max_backups", 5),
|
|
204
|
+
"rotate_days": self._config_functions("log_rotate_days", 7),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def from_dict(cls, config_dict: dict[str, Any]) -> "LoggingConfig":
|
|
209
|
+
"""
|
|
210
|
+
Create LoggingConfig from dictionary.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
config_dict: Dictionary with configuration.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
LoggingConfig instance.
|
|
217
|
+
"""
|
|
218
|
+
return cls(config_dict)
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def from_env(cls, prefix: str = "WL_") -> "LoggingConfig":
|
|
222
|
+
"""
|
|
223
|
+
Create LoggingConfig from environment variables.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
prefix: Prefix for environment variables.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
LoggingConfig instance.
|
|
230
|
+
"""
|
|
231
|
+
config_dict: dict[str, Any] = {}
|
|
232
|
+
|
|
233
|
+
# Map environment variables to config keys
|
|
234
|
+
env_mapping = {
|
|
235
|
+
f"{prefix}LOG_LEVEL": "log_level",
|
|
236
|
+
f"{prefix}LOG_CONSOLE": "log_console",
|
|
237
|
+
f"{prefix}LOG_CONSOLE_FORMAT": "log_console_format",
|
|
238
|
+
f"{prefix}LOG_FILE": "log_file",
|
|
239
|
+
f"{prefix}LOG_FILE_FORMAT": "log_file_format",
|
|
240
|
+
f"{prefix}LOG_MAX_SIZE": "log_max_size",
|
|
241
|
+
f"{prefix}LOG_MAX_BACKUPS": "log_max_backups",
|
|
242
|
+
f"{prefix}LOG_ROTATE_DAYS": "log_rotate_days",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for env_key, config_key in env_mapping.items():
|
|
246
|
+
if env_key in os.environ:
|
|
247
|
+
raw_value = os.environ[env_key]
|
|
248
|
+
# Convert boolean values
|
|
249
|
+
if raw_value.lower() in ("true", "false"):
|
|
250
|
+
config_dict[config_key] = raw_value.lower() == "true"
|
|
251
|
+
# Convert numeric values
|
|
252
|
+
elif raw_value.isdigit():
|
|
253
|
+
config_dict[config_key] = int(raw_value)
|
|
254
|
+
else:
|
|
255
|
+
config_dict[config_key] = raw_value
|
|
256
|
+
|
|
257
|
+
# Handle rotation settings
|
|
258
|
+
rotation_keys = ["log_max_size", "log_max_backups", "log_rotate_days"]
|
|
259
|
+
rotation_settings: dict[str, Any] = {}
|
|
260
|
+
for key in rotation_keys:
|
|
261
|
+
if key in config_dict:
|
|
262
|
+
rotation_settings[key.replace("log_", "")] = config_dict.pop(key)
|
|
263
|
+
|
|
264
|
+
if rotation_settings:
|
|
265
|
+
config_dict["rotation"] = rotation_settings
|
|
266
|
+
|
|
267
|
+
return cls(config_dict)
|
|
268
|
+
|
|
269
|
+
def to_dict(self) -> dict[str, Any]:
|
|
270
|
+
"""
|
|
271
|
+
Convert configuration to dictionary.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Dictionary with configuration.
|
|
275
|
+
"""
|
|
276
|
+
config_dict: dict[str, Any] = {
|
|
277
|
+
"log_level": LogLevel.get_name(self.get_log_level()),
|
|
278
|
+
"log_file": self.get_log_file(),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console_enabled, console_format = self.get_console_settings()
|
|
282
|
+
config_dict["log_console"] = console_enabled
|
|
283
|
+
config_dict["log_console_format"] = console_format
|
|
284
|
+
|
|
285
|
+
config_dict["log_file_format"] = self.get_file_format_settings()
|
|
286
|
+
config_dict["rotation"] = self.get_rotation_settings()
|
|
287
|
+
|
|
288
|
+
return config_dict
|
|
289
|
+
|
|
290
|
+
def update(self, config_dict: dict[str, Any]) -> None:
|
|
291
|
+
"""
|
|
292
|
+
Update configuration with dictionary.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
config_dict: Dictionary with configuration updates.
|
|
296
|
+
"""
|
|
297
|
+
self._explicit_config.update(config_dict)
|
|
298
|
+
# Clear cache when config is updated
|
|
299
|
+
self._clear_cache()
|
|
300
|
+
|
|
301
|
+
def _clear_cache(self) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Clear configuration cache.
|
|
304
|
+
"""
|
|
305
|
+
self._cache.clear()
|
|
306
|
+
|
|
307
|
+
def set_cache_enabled(self, enabled: bool) -> None:
|
|
308
|
+
"""
|
|
309
|
+
Enable or disable configuration caching.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
enabled: Whether to enable caching.
|
|
313
|
+
"""
|
|
314
|
+
self._cache_enabled = enabled
|
|
315
|
+
if not enabled:
|
|
316
|
+
self._clear_cache()
|
|
317
|
+
|
|
318
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
319
|
+
"""
|
|
320
|
+
Get configuration value.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
key: Configuration key.
|
|
324
|
+
default: Default value if key not found.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Configuration value or default.
|
|
328
|
+
"""
|
|
329
|
+
if key in self._explicit_config:
|
|
330
|
+
return self._explicit_config[key]
|
|
331
|
+
else:
|
|
332
|
+
return self._config_functions(key, default)
|
|
333
|
+
|
|
334
|
+
@classmethod
|
|
335
|
+
def reset(cls) -> None:
|
|
336
|
+
"""Reset the singleton instance."""
|
|
337
|
+
cls._instance = None
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Core logging functionality for WL Commands.
|
|
2
|
+
|
|
3
|
+
This module serves as the entry point for the modularized structured logging functionality.
|
|
4
|
+
主要功能委托给专门的模块以获得更好的组织结构。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..log_rotators import LogRotator
|
|
10
|
+
from .config import LoggingConfig
|
|
11
|
+
from .filtering import LogLevelChecker
|
|
12
|
+
from .formatters import LogFormatter
|
|
13
|
+
from .handlers import LogHandler
|
|
14
|
+
from .logger import StructuredLoggerCore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Backward compatibility: re-export all the main components
|
|
18
|
+
class StructuredLogger(StructuredLoggerCore):
|
|
19
|
+
"""A structured logger that outputs JSON formatted logs."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, name: str, min_level: int | None = None, log_file: str | None = None
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Initialize structured logger.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name (str): Logger name.
|
|
29
|
+
min_level (int, optional): Minimum log level. If None, read from config.
|
|
30
|
+
log_file (Optional[str]): Log file path. If None, read from config.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(name, min_level, log_file)
|
|
33
|
+
|
|
34
|
+
# Add backward compatibility properties
|
|
35
|
+
self._min_level = self.filter.min_level
|
|
36
|
+
self._log_file = self.handler.log_file
|
|
37
|
+
self.log_rotator = self.handler.log_rotator
|
|
38
|
+
self._enable_console = self.handler.enable_console
|
|
39
|
+
self.console_format = self.handler.console_format
|
|
40
|
+
self.log_file_format = self.handler.log_file_format
|
|
41
|
+
self.filters = self.filter.filters
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def min_level(self):
|
|
45
|
+
"""Get minimum log level."""
|
|
46
|
+
return self._min_level
|
|
47
|
+
|
|
48
|
+
@min_level.setter
|
|
49
|
+
def min_level(self, value):
|
|
50
|
+
"""Set minimum log level."""
|
|
51
|
+
self._min_level = value
|
|
52
|
+
self.filter.min_level = value
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def log_file(self):
|
|
56
|
+
"""Get log file path."""
|
|
57
|
+
return self._log_file
|
|
58
|
+
|
|
59
|
+
@log_file.setter
|
|
60
|
+
def log_file(self, value):
|
|
61
|
+
"""Set log file path."""
|
|
62
|
+
self._log_file = value
|
|
63
|
+
self.handler.log_file = value
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def enable_console(self):
|
|
67
|
+
"""Get console output enabled status."""
|
|
68
|
+
return self._enable_console
|
|
69
|
+
|
|
70
|
+
@enable_console.setter
|
|
71
|
+
def enable_console(self, value):
|
|
72
|
+
"""Set console output enabled status."""
|
|
73
|
+
self._enable_console = value
|
|
74
|
+
self.handler.enable_console = value
|
|
75
|
+
|
|
76
|
+
# The five verb overrides used to live here, each re-popping `message` and
|
|
77
|
+
# `lang` before calling _log. That duplication is gone (#136): _log now
|
|
78
|
+
# normalizes those kwargs once for every verb, so the inherited
|
|
79
|
+
# StructuredLoggerCore.debug/info/warning/error/critical are used directly.
|
|
80
|
+
|
|
81
|
+
# Compatibility methods for backward compatibility with tests
|
|
82
|
+
def _write_log(self, message: str) -> None:
|
|
83
|
+
"""Write log message to appropriate output (compatibility method)."""
|
|
84
|
+
import json
|
|
85
|
+
|
|
86
|
+
# Handle file writing directly for backward compatibility with tests
|
|
87
|
+
if self.log_file:
|
|
88
|
+
# Handle log rotation directly
|
|
89
|
+
if self.log_rotator and self.log_rotator.should_rotate():
|
|
90
|
+
self.log_rotator.do_rotate()
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
|
94
|
+
f.write(message + "\n")
|
|
95
|
+
except OSError:
|
|
96
|
+
# Ignore file write errors
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Handle console output directly for backward compatibility with tests
|
|
100
|
+
if self.enable_console:
|
|
101
|
+
import sys
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Format message for console based on console_format
|
|
105
|
+
console_message = message
|
|
106
|
+
if self.console_format != "json":
|
|
107
|
+
# Try to parse JSON message and reformat as human-readable
|
|
108
|
+
try:
|
|
109
|
+
log_record = json.loads(message)
|
|
110
|
+
console_message = self.formatter.format_console_log(log_record)
|
|
111
|
+
except (json.JSONDecodeError, TypeError):
|
|
112
|
+
# Fallback to raw message if parsing fails
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
# Determine output stream based on log level in message
|
|
116
|
+
if "CRITICAL" in message or "ERROR" in message:
|
|
117
|
+
print(console_message, file=sys.stderr)
|
|
118
|
+
else:
|
|
119
|
+
print(console_message, file=sys.stdout)
|
|
120
|
+
except OSError:
|
|
121
|
+
# Ignore console write errors
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
def _write_log_with_rotation(self, message: str) -> None:
|
|
125
|
+
"""Write log message with rotation (compatibility method)."""
|
|
126
|
+
self._write_log(message)
|
|
127
|
+
|
|
128
|
+
def _format_human_log(self, record: dict[str, Any]) -> str:
|
|
129
|
+
"""Format log record for human-readable file output (compatibility method)."""
|
|
130
|
+
return self.formatter.format_human_log(record)
|
|
131
|
+
|
|
132
|
+
def _format_console_log(self, record: dict[str, Any]) -> str:
|
|
133
|
+
"""Format log record for console output (compatibility method)."""
|
|
134
|
+
return self.formatter.format_console_log(record)
|
|
135
|
+
|
|
136
|
+
# Property accessors for backward compatibility
|
|
137
|
+
@property
|
|
138
|
+
def _COLORS(
|
|
139
|
+
self,
|
|
140
|
+
) -> dict[str, str]:
|
|
141
|
+
"""Get color codes (compatibility property)."""
|
|
142
|
+
return self.formatter._COLORS
|
|
143
|
+
|
|
144
|
+
@_COLORS.setter
|
|
145
|
+
def _COLORS(self, value: dict[str, str]) -> None:
|
|
146
|
+
"""Set color codes (compatibility property)."""
|
|
147
|
+
self.formatter._COLORS = value
|
|
148
|
+
|
|
149
|
+
def _should_log(self, level: int, **kwargs) -> bool:
|
|
150
|
+
"""Check if log should be processed based on level and filters (compatibility method)."""
|
|
151
|
+
return self.filter.should_log(level, **kwargs)
|
|
152
|
+
|
|
153
|
+
def _get_level_name(self, level: int) -> str:
|
|
154
|
+
"""Convert level number to name (compatibility method)."""
|
|
155
|
+
return self.filter.get_level_name(level)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def filters(self):
|
|
159
|
+
"""Get filters for backward compatibility."""
|
|
160
|
+
return self.filter.filters
|
|
161
|
+
|
|
162
|
+
@filters.setter
|
|
163
|
+
def filters(self, value):
|
|
164
|
+
"""Set filters for backward compatibility."""
|
|
165
|
+
self.filter.filters = value
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Re-export classes for advanced usage
|
|
169
|
+
__all__ = [
|
|
170
|
+
"StructuredLogger",
|
|
171
|
+
"StructuredLoggerCore",
|
|
172
|
+
"LoggingConfig",
|
|
173
|
+
"LogLevelChecker",
|
|
174
|
+
"LogFormatter",
|
|
175
|
+
"LogHandler",
|
|
176
|
+
"LogRotator",
|
|
177
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Test-environment predicates shared across the logging stack (#136, #142).
|
|
2
|
+
|
|
3
|
+
"Are we under pytest? Is this a performance test?" used to be answered
|
|
4
|
+
independently at every site that cared: the emit funnel
|
|
5
|
+
(:mod:`wl_cli.utils.logging`), several places downstream in
|
|
6
|
+
:class:`~wl_cli.utils.structured_logging.logger.StructuredLoggerCore`, and the
|
|
7
|
+
logging config / handler internals. They all now consult this module, so the
|
|
8
|
+
judgment is written exactly once and a change to it cannot drift between the
|
|
9
|
+
callers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"is_performance_test",
|
|
16
|
+
"is_pytest",
|
|
17
|
+
"is_test_environment",
|
|
18
|
+
"pytest_test_name",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pytest_test_name() -> str:
|
|
23
|
+
"""Return pytest's per-test node id, or ``""`` outside a running test."""
|
|
24
|
+
return os.environ.get("PYTEST_CURRENT_TEST", "")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_pytest() -> bool:
|
|
28
|
+
"""True while pytest's per-test hook is active (plain or xdist worker)."""
|
|
29
|
+
return os.environ.get("PYTEST_CURRENT_TEST") is not None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_performance_test() -> bool:
|
|
33
|
+
"""True for the throughput suites that deliberately skip log I/O.
|
|
34
|
+
|
|
35
|
+
Matches any node id whose file / class / function mentions ``performance``;
|
|
36
|
+
that covers both ``test_log_performance.py`` and
|
|
37
|
+
``test_logging_performance.py``. This is a superset of the narrower
|
|
38
|
+
predicate the funnel used to carry, and is the one the logger already used
|
|
39
|
+
to suppress output, so routing the funnel through it changes nothing
|
|
40
|
+
observable.
|
|
41
|
+
"""
|
|
42
|
+
return "performance" in pytest_test_name().lower()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_test_environment() -> bool:
|
|
46
|
+
"""Broad "an automated/test run" gate used for logger defaults.
|
|
47
|
+
|
|
48
|
+
Covers pytest (plain and xdist) plus the ``NON_INTERACTIVE`` escape hatch
|
|
49
|
+
that ``wl test`` sets, and drives default level / file / console choices.
|
|
50
|
+
"""
|
|
51
|
+
return (
|
|
52
|
+
is_pytest()
|
|
53
|
+
or os.environ.get("PYTEST_XDIST_WORKER") is not None
|
|
54
|
+
or os.environ.get("PYTEST_XDIST_NUM_WORKERS") is not None
|
|
55
|
+
or os.environ.get("NON_INTERACTIVE") is not None
|
|
56
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Filtering and level checking for structured logging."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..log_levels import LogLevel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogLevelChecker:
|
|
11
|
+
"""Manages filtering and level checking for structured logging."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, min_level: int) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Initialize log level checker.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
min_level: Minimum log level.
|
|
19
|
+
"""
|
|
20
|
+
self.min_level = min_level
|
|
21
|
+
self.filters: list[Callable[[dict[str, Any]], bool]] = []
|
|
22
|
+
|
|
23
|
+
def add_filter(self, filter_func: Callable[[dict[str, Any]], bool]) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Add a filter function.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
filter_func: Function that takes a log record and returns bool.
|
|
29
|
+
"""
|
|
30
|
+
self.filters.append(filter_func)
|
|
31
|
+
|
|
32
|
+
def should_log(self, level: int, **kwargs) -> bool:
|
|
33
|
+
"""
|
|
34
|
+
Check if log should be processed based on level and filters.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
level: Log level.
|
|
38
|
+
**kwargs: Log record fields.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if log should be processed.
|
|
42
|
+
"""
|
|
43
|
+
if level < self.min_level:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
# Only create record if level is sufficient
|
|
47
|
+
if not self.filters:
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
# Create record with timestamp only if there are filters to apply
|
|
51
|
+
record = {
|
|
52
|
+
"level": level,
|
|
53
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
54
|
+
**kwargs,
|
|
55
|
+
}
|
|
56
|
+
return all(f(record) for f in self.filters)
|
|
57
|
+
|
|
58
|
+
def get_level_name(self, level: int) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Convert level number to name.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
level: Log level number.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Log level name as string.
|
|
67
|
+
"""
|
|
68
|
+
level_names = {
|
|
69
|
+
LogLevel.DEBUG: "DEBUG",
|
|
70
|
+
LogLevel.INFO: "INFO",
|
|
71
|
+
LogLevel.WARNING: "WARNING",
|
|
72
|
+
LogLevel.ERROR: "ERROR",
|
|
73
|
+
LogLevel.CRITICAL: "CRITICAL",
|
|
74
|
+
}
|
|
75
|
+
return level_names.get(level, "UNKNOWN")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Log filters for WL Commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LogFilter:
|
|
7
|
+
"""Base class for log filters."""
|
|
8
|
+
|
|
9
|
+
def __call__(self, record: dict[str, Any]) -> bool:
|
|
10
|
+
"""Determine if the log record should be processed."""
|
|
11
|
+
raise NotImplementedError("Subclasses must implement __call__ method.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LevelFilter(LogFilter):
|
|
15
|
+
"""Filter logs based on log level."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, min_level: int, max_level: int | None = None) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Initialize level filter.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
min_level (int): Minimum log level to include.
|
|
23
|
+
max_level (Optional[int]): Maximum log level to include.
|
|
24
|
+
"""
|
|
25
|
+
self.min_level = min_level
|
|
26
|
+
self.max_level = max_level
|
|
27
|
+
|
|
28
|
+
def __call__(self, record: dict[str, Any]) -> bool:
|
|
29
|
+
"""Check if log record level is within specified range."""
|
|
30
|
+
level = record.get("level", 0)
|
|
31
|
+
if level < self.min_level:
|
|
32
|
+
return False
|
|
33
|
+
if self.max_level is not None and level > self.max_level:
|
|
34
|
+
return False
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class KeyValueFilter(LogFilter):
|
|
39
|
+
"""Filter logs based on key-value pairs."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, **kwargs) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Initialize key-value filter.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
**kwargs: Key-value pairs to match in log records.
|
|
47
|
+
"""
|
|
48
|
+
self.filters = kwargs
|
|
49
|
+
|
|
50
|
+
def __call__(self, record: dict[str, Any]) -> bool:
|
|
51
|
+
"""Check if log record contains all specified key-value pairs."""
|
|
52
|
+
for key, value in self.filters.items():
|
|
53
|
+
if record.get(key) != value:
|
|
54
|
+
return False
|
|
55
|
+
return True
|