"""Utility to discover and run all diagnostic rules using Docker.""" import importlib import inspect import pkgutil from datetime import datetime from inspect import signature from pathlib import Path from types import ModuleType from app.logcli.doctor_context import DoctorContext DIAGNOSTICS_PACKAGE = 'app.logcli.diagnostics' RULE_PREFIX = 'rule_' async def _run_rule(rule_cls, ctx: DoctorContext): """Run a single diagnostic rule class instance with context. Returns: dict: The result of the rule execution """ try: rule = rule_cls() # Determine if rule.run expects a context parameter params = list(signature(rule.run).parameters) if len(params) == 2: # self, ctx return await rule.run(ctx) else: return await rule.run() except Exception as exc: return { 'status': 'error', 'error': str(exc), 'message': f"Error executing rule: {str(exc)}" } async def _discover_rule_classes() -> list: """Discover all rule classes in diagnostics package.""" try: pkg: ModuleType = importlib.import_module(DIAGNOSTICS_PACKAGE) # Handle case where __file__ is None (namespace packages) if getattr(pkg, '__file__', None) is None: # Try to find the package path using pkgutil loader = pkgutil.get_loader(DIAGNOSTICS_PACKAGE) if loader is None: print(f"Warning: Could not find loader for package: {DIAGNOSTICS_PACKAGE}") return [] # Get the package path from the loader pkg_path = Path(loader.path).parent if hasattr(loader, 'path') else None else: pkg_path = Path(pkg.__file__).parent if pkg_path is None or not pkg_path.exists(): print(f"Warning: Could not locate package directory for: {DIAGNOSTICS_PACKAGE}") return [] rule_classes = [] package_dir = str(pkg_path) # Print debug info print(f"Looking for rules in: {package_dir}") # Keep track of already seen rule classes to avoid duplicates seen_classes = set() for finder, name, is_pkg in pkgutil.iter_modules([package_dir]): if name.startswith(RULE_PREFIX) and not name.startswith('__'): module_name = f"{DIAGNOSTICS_PACKAGE}.{name}" try: module = importlib.import_module(module_name) for _, obj in inspect.getmembers(module, inspect.isclass): # Only include classes defined in this module (not imported ones) # and that have an async run method if (hasattr(obj, 'run') and inspect.iscoroutinefunction(obj.run) and obj.__module__ == module_name): # Use the full module path + class name as unique identifier class_id = f"{obj.__module__}.{obj.__name__}" if class_id not in seen_classes: seen_classes.add(class_id) rule_classes.append(obj) except ImportError as e: print(f"Error importing module {module_name}: {e}") continue return rule_classes except Exception as e: print(f"Error discovering rule classes: {e}") return [] def _generate_report_filename() -> str: """Generate a report filename with timestamp.""" from datetime import datetime timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") return f"diagnostic_report_{timestamp}.json" async def run_all_rules(): """Discover and run all diagnostic rules sequentially. Returns: tuple: (results, report_file) - A tuple containing the results and the report filename """ rule_classes = await _discover_rule_classes() if not rule_classes: print("No diagnostic rules found.") return [], None ctx = DoctorContext.create() results = [] report_data = { 'timestamp': datetime.utcnow().isoformat(), 'rules_run': [], 'summary': {} } # Print summary of discovered rules print(f"Discovered {len(rule_classes)} rule(s) to run:") for i, cls in enumerate(rule_classes, 1): print(f" {i}. {cls.__name__} ({cls.__module__})") # Run each rule for cls in rule_classes: rule_name = cls.__name__ result = await _run_rule(cls, ctx) # Prepare rule result rule_result = { 'rule': f"{cls.__module__}.{rule_name}", 'status': result.get('status', 'unknown'), 'message': result.get('message', ''), 'issues_count': len(result.get('issues', [])), 'execution_time': result.get('execution_time', 0) } # Add to results results.append(rule_result) report_data['rules_run'].append({ **rule_result, 'issues': result.get('issues', []), 'metadata': result.get('metadata', {}) }) # Print status if result.get('status') == 'error': print(f"✗ {rule_name}: {result.get('error', 'Unknown error')}") elif result.get('issues'): print(f"✗ {rule_name}: {result.get('message', 'Issues found')} ({len(result['issues'])} issues)") else: print(f"✓ {rule_name}") # Generate summary total_rules = len(results) passed_rules = sum(1 for r in results if r.get('status') == 'success' and not r.get('issues_count', 0)) failed_rules = total_rules - passed_rules summary = { 'total_rules': total_rules, 'passed': passed_rules, 'failed': failed_rules, 'success_rate': f"{(passed_rules / total_rules * 100):.1f}%" if total_rules > 0 else 'N/A' } report_data['summary'] = summary # Save detailed report to file report_file = None if any(r.get('issues_count', 0) > 0 for r in results): import json report_file = _generate_report_filename() with open(report_file, 'w', encoding='utf-8') as f: json.dump(report_data, f, indent=2, ensure_ascii=False, default=str) print(f"\nDetailed report saved to: {report_file}") # Print CLI summary print(f"\nSummary: {passed_rules} passed, {failed_rules} failed") if failed_rules > 0: print("Run with --report to view detailed issues") return results, report_file