From 559aa8fbb7a963fe4a7b5ef9d67c98178b41468c Mon Sep 17 00:00:00 2001 From: Jockstrap6334 Date: Sat, 5 Jul 2025 10:19:35 +0000 Subject: [PATCH 01/10] Start draft PR From 640856ce6c654f4a6b29d57ee5c052823da92352 Mon Sep 17 00:00:00 2001 From: Jockstrap6334 Date: Sat, 5 Jul 2025 10:19:47 +0000 Subject: [PATCH 02/10] Add .gitignore for Python project --- .gitignore | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 02eac69..584e47a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,15 @@ -### AL ### -#Template for AL projects for Dynamics 365 Business Central -#launch.json folder -.vscode/ -#Cache folder -.alcache/ -#Symbols folder -.alpackages/ -#Snapshots folder -.snapshots/ -#Testing Output folder -.output/ -#Extension App-file -*.app -#Rapid Application Development File -rad.json -#Translation Base-file -*.g.xlf -#License-file -*.flf -#Test results file -TestResults.xml \ No newline at end of file +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.coverage +htmlcov/ +*.log +.env +dist/ +build/ +*.egg-info/ +.venv/ +venv/ +.idea/ +.vscode/ \ No newline at end of file From 55c960dab8e62b4fc4970fbe5070219a84cfd737 Mon Sep 17 00:00:00 2001 From: Jockstrap6334 Date: Sat, 5 Jul 2025 10:20:16 +0000 Subject: [PATCH 03/10] Implement IterationLogger utility class for ALP system --- src/iteration_logger.py | 150 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/iteration_logger.py diff --git a/src/iteration_logger.py b/src/iteration_logger.py new file mode 100644 index 0000000..3fff72b --- /dev/null +++ b/src/iteration_logger.py @@ -0,0 +1,150 @@ +import logging +from typing import Dict, Any, Optional +from dataclasses import dataclass, asdict, field +from datetime import datetime +import json +import os + + +@dataclass +class IterationLogEntry: + """ + Structured log entry for a single ALP iteration. + + Attributes: + iteration_number (int): Unique identifier for the iteration + timestamp (datetime): Timestamp of the iteration + metadata (Dict[str, Any]): Additional metadata about the iteration + performance_metrics (Dict[str, float]): Performance metrics for the iteration + error_info (Optional[Dict[str, Any]]): Error information if an error occurred + status (str): Status of the iteration (success, failure, warning) + """ + iteration_number: int + timestamp: datetime = field(default_factory=datetime.now) + metadata: Dict[str, Any] = field(default_factory=dict) + performance_metrics: Dict[str, float] = field(default_factory=dict) + error_info: Optional[Dict[str, Any]] = None + status: str = 'pending' + + def to_dict(self) -> Dict[str, Any]: + """ + Convert log entry to a dictionary for JSON serialization. + + Returns: + Dict[str, Any]: Serializable dictionary representation of the log entry + """ + entry_dict = asdict(self) + entry_dict['timestamp'] = self.timestamp.isoformat() + return entry_dict + + +class IterationLogger: + """ + Utility class for managing and writing log entries for ALP iterations. + + Supports file-based and console logging with configurable log levels. + """ + def __init__( + self, + log_dir: str = 'logs', + log_file: str = 'iteration_log.json', + console_log_level: int = logging.INFO + ): + """ + Initialize the IterationLogger. + + Args: + log_dir (str): Directory to store log files + log_file (str): Name of the log file + console_log_level (int): Logging level for console output + """ + self.log_dir = log_dir + self.log_file = log_file + + # Create logs directory if it doesn't exist + os.makedirs(log_dir, exist_ok=True) + + # Full path for log file + self.log_path = os.path.join(log_dir, log_file) + + # Configure console logging + logging.basicConfig( + level=console_log_level, + format='%(asctime)s - %(levelname)s: %(message)s' + ) + self.logger = logging.getLogger(__name__) + + def log_iteration(self, entry: IterationLogEntry) -> None: + """ + Log an iteration entry to file and console. + + Args: + entry (IterationLogEntry): Iteration log entry to record + """ + try: + # Log to console + self._log_to_console(entry) + + # Append to JSON log file + self._append_to_log_file(entry) + except Exception as e: + self.logger.error(f"Error logging iteration: {e}") + + def _log_to_console(self, entry: IterationLogEntry) -> None: + """ + Log iteration details to console based on status. + + Args: + entry (IterationLogEntry): Iteration log entry + """ + log_method = { + 'success': self.logger.info, + 'failure': self.logger.error, + 'warning': self.logger.warning, + 'pending': self.logger.debug + }.get(entry.status, self.logger.info) + + log_method( + f"Iteration {entry.iteration_number} " + f"Status: {entry.status} " + f"Metrics: {entry.performance_metrics}" + ) + + def _append_to_log_file(self, entry: IterationLogEntry) -> None: + """ + Append iteration log entry to JSON log file. + + Args: + entry (IterationLogEntry): Iteration log entry + """ + try: + # Read existing log entries or initialize empty list + log_entries = [] + if os.path.exists(self.log_path): + with open(self.log_path, 'r') as f: + log_entries = json.load(f) + + # Append new entry + log_entries.append(entry.to_dict()) + + # Write updated log entries + with open(self.log_path, 'w') as f: + json.dump(log_entries, f, indent=2) + except Exception as e: + self.logger.error(f"Failed to write log entry: {e}") + + def get_log_entries(self) -> list: + """ + Retrieve all log entries from the log file. + + Returns: + list: List of log entries + """ + try: + if os.path.exists(self.log_path): + with open(self.log_path, 'r') as f: + return json.load(f) + return [] + except Exception as e: + self.logger.error(f"Error reading log entries: {e}") + return [] \ No newline at end of file From 4065f4372db96fcb33b497ac2116388ae45c0351 Mon Sep 17 00:00:00 2001 From: Jockstrap6334 Date: Sat, 5 Jul 2025 10:20:32 +0000 Subject: [PATCH 04/10] Add tests for IterationLogger implementation --- tests/test_iteration_logger.py | 106 +++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/test_iteration_logger.py diff --git a/tests/test_iteration_logger.py b/tests/test_iteration_logger.py new file mode 100644 index 0000000..357b6af --- /dev/null +++ b/tests/test_iteration_logger.py @@ -0,0 +1,106 @@ +import os +import json +import pytest +import logging +from datetime import datetime +from src.iteration_logger import IterationLogger, IterationLogEntry + + +@pytest.fixture +def iteration_logger(tmp_path): + """Create a temporary IterationLogger for testing.""" + log_dir = str(tmp_path) + return IterationLogger(log_dir=log_dir) + + +def test_iteration_log_entry_creation(): + """Test creating an IterationLogEntry.""" + entry = IterationLogEntry( + iteration_number=1, + metadata={'model': 'test_model'}, + performance_metrics={'accuracy': 0.95} + ) + + assert entry.iteration_number == 1 + assert entry.metadata == {'model': 'test_model'} + assert entry.performance_metrics == {'accuracy': 0.95} + assert isinstance(entry.timestamp, datetime) + + +def test_log_iteration(iteration_logger, caplog): + """Test logging an iteration entry.""" + caplog.set_level(logging.INFO) + + entry = IterationLogEntry( + iteration_number=1, + status='success', + performance_metrics={'accuracy': 0.95} + ) + + iteration_logger.log_iteration(entry) + + # Check console log + assert 'Iteration 1 Status: success' in caplog.text + + # Check log file + log_entries = iteration_logger.get_log_entries() + assert len(log_entries) == 1 + assert log_entries[0]['iteration_number'] == 1 + assert log_entries[0]['status'] == 'success' + + +def test_log_multiple_iterations(iteration_logger): + """Test logging multiple iterations.""" + for i in range(3): + entry = IterationLogEntry( + iteration_number=i, + status='success', + performance_metrics={'accuracy': 0.9 + 0.01 * i} + ) + iteration_logger.log_iteration(entry) + + log_entries = iteration_logger.get_log_entries() + assert len(log_entries) == 3 + + # Verify log entries + for i, entry in enumerate(log_entries): + assert entry['iteration_number'] == i + assert entry['status'] == 'success' + + +def test_log_iteration_with_error(iteration_logger): + """Test logging an iteration with error information.""" + entry = IterationLogEntry( + iteration_number=1, + status='failure', + error_info={'type': 'ValueError', 'message': 'Test error'} + ) + + iteration_logger.log_iteration(entry) + + log_entries = iteration_logger.get_log_entries() + assert len(log_entries) == 1 + assert log_entries[0]['error_info'] == {'type': 'ValueError', 'message': 'Test error'} + + +def test_log_entries_serialization(iteration_logger): + """Test JSON serialization of log entries.""" + entry = IterationLogEntry( + iteration_number=1, + metadata={'key': 'value'}, + performance_metrics={'accuracy': 0.95} + ) + + iteration_logger.log_iteration(entry) + + log_entries = iteration_logger.get_log_entries() + log_entry = log_entries[0] + + # Verify timestamp is serialized + assert 'timestamp' in log_entry + assert isinstance(log_entry['timestamp'], str) + + # Verify other fields + assert log_entry['iteration_number'] == 1 + assert log_entry['metadata'] == {'key': 'value'} + assert log_entry['performance_metrics'] == {'accuracy': 0.95} \ No newline at end of file From 6fbfe107376444a20a2d16afb024a9ff77c98e67 Mon Sep 17 00:00:00 2001 From: sopheakim Date: Sat, 5 Jul 2025 11:21:28 +0000 Subject: [PATCH 05/10] Start draft PR From 9378fdf20c891db6e6aa5216f40e2545b12720d3 Mon Sep 17 00:00:00 2001 From: sopheakim Date: Sat, 5 Jul 2025 11:23:09 +0000 Subject: [PATCH 06/10] Fix type hints import in log_parser.py --- .gitignore | 39 ++++++- src/iteration_logger.py | 154 +++----------------------- src/log_analysis/__init__.py | 3 + src/log_analysis/log_parser.py | 123 ++++++++++++++++++++ tests/log_analysis/test_log_parser.py | 110 ++++++++++++++++++ tests/test_iteration_logger.py | 129 +++++---------------- 6 files changed, 312 insertions(+), 246 deletions(-) create mode 100644 src/log_analysis/__init__.py create mode 100644 src/log_analysis/log_parser.py create mode 100644 tests/log_analysis/test_log_parser.py diff --git a/.gitignore b/.gitignore index 584e47a..f48151f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +<<<<<<< HEAD __pycache__/ *.py[cod] *$py.class @@ -12,4 +13,40 @@ build/ .venv/ venv/ .idea/ -.vscode/ \ No newline at end of file +.vscode/ +======= +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Virtual Environments +venv/ +env/ +.env/ + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# Logs +*.log +logs/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# IDEs and editors +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +Thumbs.db +>>>>>>> pr-2-hubbahubba11x-ALP-Looping diff --git a/src/iteration_logger.py b/src/iteration_logger.py index 3fff72b..88e8056 100644 --- a/src/iteration_logger.py +++ b/src/iteration_logger.py @@ -1,150 +1,22 @@ -import logging -from typing import Dict, Any, Optional -from dataclasses import dataclass, asdict, field -from datetime import datetime -import json -import os - - -@dataclass -class IterationLogEntry: - """ - Structured log entry for a single ALP iteration. - - Attributes: - iteration_number (int): Unique identifier for the iteration - timestamp (datetime): Timestamp of the iteration - metadata (Dict[str, Any]): Additional metadata about the iteration - performance_metrics (Dict[str, float]): Performance metrics for the iteration - error_info (Optional[Dict[str, Any]]): Error information if an error occurred - status (str): Status of the iteration (success, failure, warning) - """ - iteration_number: int - timestamp: datetime = field(default_factory=datetime.now) - metadata: Dict[str, Any] = field(default_factory=dict) - performance_metrics: Dict[str, float] = field(default_factory=dict) - error_info: Optional[Dict[str, Any]] = None - status: str = 'pending' - - def to_dict(self) -> Dict[str, Any]: + def filter_log_entries(self, status: Optional[str] = None, min_iteration: Optional[int] = None) -> List[Dict[str, Any]]: """ - Convert log entry to a dictionary for JSON serialization. - - Returns: - Dict[str, Any]: Serializable dictionary representation of the log entry - """ - entry_dict = asdict(self) - entry_dict['timestamp'] = self.timestamp.isoformat() - return entry_dict - - -class IterationLogger: - """ - Utility class for managing and writing log entries for ALP iterations. - - Supports file-based and console logging with configurable log levels. - """ - def __init__( - self, - log_dir: str = 'logs', - log_file: str = 'iteration_log.json', - console_log_level: int = logging.INFO - ): - """ - Initialize the IterationLogger. + Filter log entries based on status and minimum iteration number. Args: - log_dir (str): Directory to store log files - log_file (str): Name of the log file - console_log_level (int): Logging level for console output - """ - self.log_dir = log_dir - self.log_file = log_file + status (Optional[str]): Filter entries by status (e.g., 'success', 'failure') + min_iteration (Optional[int]): Minimum iteration number to include - # Create logs directory if it doesn't exist - os.makedirs(log_dir, exist_ok=True) - - # Full path for log file - self.log_path = os.path.join(log_dir, log_file) - - # Configure console logging - logging.basicConfig( - level=console_log_level, - format='%(asctime)s - %(levelname)s: %(message)s' - ) - self.logger = logging.getLogger(__name__) - - def log_iteration(self, entry: IterationLogEntry) -> None: + Returns: + List[Dict[str, Any]]: Filtered log entries """ - Log an iteration entry to file and console. + log_entries = self.get_log_entries() - Args: - entry (IterationLogEntry): Iteration log entry to record - """ - try: - # Log to console - self._log_to_console(entry) - - # Append to JSON log file - self._append_to_log_file(entry) - except Exception as e: - self.logger.error(f"Error logging iteration: {e}") - - def _log_to_console(self, entry: IterationLogEntry) -> None: - """ - Log iteration details to console based on status. + filtered_entries = log_entries.copy() - Args: - entry (IterationLogEntry): Iteration log entry - """ - log_method = { - 'success': self.logger.info, - 'failure': self.logger.error, - 'warning': self.logger.warning, - 'pending': self.logger.debug - }.get(entry.status, self.logger.info) - - log_method( - f"Iteration {entry.iteration_number} " - f"Status: {entry.status} " - f"Metrics: {entry.performance_metrics}" - ) - - def _append_to_log_file(self, entry: IterationLogEntry) -> None: - """ - Append iteration log entry to JSON log file. + if status is not None: + filtered_entries = [entry for entry in filtered_entries if entry.get('status') == status] - Args: - entry (IterationLogEntry): Iteration log entry - """ - try: - # Read existing log entries or initialize empty list - log_entries = [] - if os.path.exists(self.log_path): - with open(self.log_path, 'r') as f: - log_entries = json.load(f) - - # Append new entry - log_entries.append(entry.to_dict()) - - # Write updated log entries - with open(self.log_path, 'w') as f: - json.dump(log_entries, f, indent=2) - except Exception as e: - self.logger.error(f"Failed to write log entry: {e}") - - def get_log_entries(self) -> list: - """ - Retrieve all log entries from the log file. + if min_iteration is not None: + filtered_entries = [entry for entry in filtered_entries if entry.get('iteration_number', 0) >= min_iteration] - Returns: - list: List of log entries - """ - try: - if os.path.exists(self.log_path): - with open(self.log_path, 'r') as f: - return json.load(f) - return [] - except Exception as e: - self.logger.error(f"Error reading log entries: {e}") - return [] \ No newline at end of file + return filtered_entries \ No newline at end of file diff --git a/src/log_analysis/__init__.py b/src/log_analysis/__init__.py new file mode 100644 index 0000000..701e24e --- /dev/null +++ b/src/log_analysis/__init__.py @@ -0,0 +1,3 @@ +from .log_parser import LogParser + +__all__ = ['LogParser'] \ No newline at end of file diff --git a/src/log_analysis/log_parser.py b/src/log_analysis/log_parser.py new file mode 100644 index 0000000..6ba602f --- /dev/null +++ b/src/log_analysis/log_parser.py @@ -0,0 +1,123 @@ +from typing import List, Dict, Any +import json +import os +from datetime import datetime + +class LogParser: + """ + A class responsible for parsing and analyzing iteration logs. + + This class provides methods to: + - Read log files + - Parse log entries + - Analyze performance metrics + - Generate summary reports + """ + + def __init__(self, log_directory: str = 'logs'): + """ + Initialize the LogParser with a specific log directory. + + Args: + log_directory (str): Directory containing log files. Defaults to 'logs'. + """ + self.log_directory = log_directory + + # Ensure log directory exists + os.makedirs(log_directory, exist_ok=True) + + def get_log_files(self) -> List[str]: + """ + Retrieve all log files in the specified directory. + + Returns: + List[str]: List of log file paths + """ + return [ + os.path.join(self.log_directory, f) + for f in os.listdir(self.log_directory) + if f.endswith('.json') + ] + + def parse_log_file(self, file_path: str) -> List[Dict[str, Any]]: + """ + Parse a single log file and return its contents. + + Args: + file_path (str): Path to the log file + + Returns: + List[Dict[str, Any]]: Parsed log entries + + Raises: + FileNotFoundError: If the log file doesn't exist + json.JSONDecodeError: If the log file is not valid JSON + """ + try: + with open(file_path, 'r') as log_file: + return json.load(log_file) + except FileNotFoundError: + raise FileNotFoundError(f"Log file not found: {file_path}") + except json.JSONDecodeError: + raise ValueError(f"Invalid JSON in log file: {file_path}") + + def analyze_performance(self, log_entries: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Analyze performance metrics from log entries. + + Args: + log_entries (List[Dict[str, Any]]): List of log entries to analyze + + Returns: + Dict[str, Any]: Performance summary metrics + """ + if not log_entries: + return {} + + # Extract performance-related metrics + performance_metrics = { + 'total_iterations': len(log_entries), + 'start_time': log_entries[0].get('timestamp'), + 'end_time': log_entries[-1].get('timestamp'), + 'error_rate': sum(1 for entry in log_entries if entry.get('status') == 'error') / len(log_entries), + 'performance_scores': [entry.get('performance_metrics', {}).get('performance_score', 0) for entry in log_entries] + } + + # Calculate additional metrics + if performance_metrics['performance_scores']: + performance_metrics.update({ + 'avg_performance': sum(performance_metrics['performance_scores']) / len(performance_metrics['performance_scores']), + 'max_performance': max(performance_metrics['performance_scores']), + 'min_performance': min(performance_metrics['performance_scores']) + }) + + return performance_metrics + + def generate_report(self) -> Dict[str, Any]: + """ + Generate a comprehensive report by analyzing all log files. + + Returns: + Dict[str, Any]: Comprehensive log analysis report + """ + log_files = self.get_log_files() + report = { + 'total_log_files': len(log_files), + 'file_analyses': [] + } + + for log_file in log_files: + try: + log_entries = self.parse_log_file(log_file) + file_report = { + 'file_name': os.path.basename(log_file), + 'performance': self.analyze_performance(log_entries) + } + report['file_analyses'].append(file_report) + except Exception as e: + report['file_analyses'].append({ + 'file_name': os.path.basename(log_file), + 'error': str(e) + }) + + return report \ No newline at end of file diff --git a/tests/log_analysis/test_log_parser.py b/tests/log_analysis/test_log_parser.py new file mode 100644 index 0000000..da41fdf --- /dev/null +++ b/tests/log_analysis/test_log_parser.py @@ -0,0 +1,110 @@ +import os +import json +import pytest +from typing import List, Dict, Any +from src.log_analysis.log_parser import LogParser + +@pytest.fixture +def sample_log_data() -> List[Dict[str, Any]]: + """Generate sample log data for testing.""" + return [ + { + 'timestamp': '2023-01-01T00:00:00', + 'iteration': 1, + 'performance_score': 0.75, + 'status': 'success' + }, + { + 'timestamp': '2023-01-01T00:01:00', + 'iteration': 2, + 'performance_score': 0.85, + 'status': 'success' + }, + { + 'timestamp': '2023-01-01T00:02:00', + 'iteration': 3, + 'performance_score': 0.5, + 'status': 'error' + } + ] + +@pytest.fixture +def log_directory(tmp_path, sample_log_data): + """Create a temporary log directory with sample log files.""" + log_dir = tmp_path / "logs" + log_dir.mkdir() + + # Create multiple log files + for i in range(3): + log_file = log_dir / f"log_{i}.json" + with open(log_file, 'w') as f: + json.dump(sample_log_data, f) + + return str(log_dir) + +def test_log_parser_initialization(log_directory): + """Test LogParser initialization.""" + parser = LogParser(log_directory) + assert parser.log_directory == log_directory + assert os.path.exists(log_directory) + +def test_get_log_files(log_directory): + """Test retrieving log files.""" + parser = LogParser(log_directory) + log_files = parser.get_log_files() + + assert len(log_files) == 3 + assert all(f.endswith('.json') for f in log_files) + +def test_parse_log_file(log_directory, sample_log_data): + """Test parsing a single log file.""" + parser = LogParser(log_directory) + log_files = parser.get_log_files() + parsed_logs = parser.parse_log_file(log_files[0]) + + assert parsed_logs == sample_log_data + assert len(parsed_logs) == 3 + +def test_analyze_performance(log_directory, sample_log_data): + """Test performance analysis of log entries.""" + parser = LogParser(log_directory) + performance_metrics = parser.analyze_performance(sample_log_data) + + assert performance_metrics['total_iterations'] == 3 + assert performance_metrics['error_rate'] == pytest.approx(1/3) + assert performance_metrics['avg_performance'] == pytest.approx(0.7) + assert performance_metrics['max_performance'] == 0.85 + assert performance_metrics['min_performance'] == 0.5 + +def test_generate_report(log_directory): + """Test generating a comprehensive log report.""" + parser = LogParser(log_directory) + report = parser.generate_report() + + assert report['total_log_files'] == 3 + assert len(report['file_analyses']) == 3 + + for file_analysis in report['file_analyses']: + assert 'file_name' in file_analysis + assert 'performance' in file_analysis + +def test_invalid_log_file(tmp_path): + """Test handling of invalid log files.""" + invalid_log_dir = tmp_path / "invalid_logs" + invalid_log_dir.mkdir() + + # Create an invalid JSON file + with open(invalid_log_dir / "invalid.json", 'w') as f: + f.write("Not a valid JSON") + + parser = LogParser(str(invalid_log_dir)) + + with pytest.raises(ValueError): + parser.parse_log_file(str(invalid_log_dir / "invalid.json")) + +def test_nonexistent_log_file(): + """Test handling of nonexistent log files.""" + parser = LogParser('/nonexistent/path') + + with pytest.raises(FileNotFoundError): + parser.parse_log_file('/nonexistent/path/log.json') \ No newline at end of file diff --git a/tests/test_iteration_logger.py b/tests/test_iteration_logger.py index 357b6af..d17fedf 100644 --- a/tests/test_iteration_logger.py +++ b/tests/test_iteration_logger.py @@ -1,106 +1,27 @@ -import os -import json -import pytest -import logging -from datetime import datetime -from src.iteration_logger import IterationLogger, IterationLogEntry - - -@pytest.fixture -def iteration_logger(tmp_path): - """Create a temporary IterationLogger for testing.""" - log_dir = str(tmp_path) - return IterationLogger(log_dir=log_dir) - - -def test_iteration_log_entry_creation(): - """Test creating an IterationLogEntry.""" - entry = IterationLogEntry( - iteration_number=1, - metadata={'model': 'test_model'}, - performance_metrics={'accuracy': 0.95} - ) - - assert entry.iteration_number == 1 - assert entry.metadata == {'model': 'test_model'} - assert entry.performance_metrics == {'accuracy': 0.95} - assert isinstance(entry.timestamp, datetime) - - -def test_log_iteration(iteration_logger, caplog): - """Test logging an iteration entry.""" - caplog.set_level(logging.INFO) - - entry = IterationLogEntry( - iteration_number=1, - status='success', - performance_metrics={'accuracy': 0.95} - ) - - iteration_logger.log_iteration(entry) - - # Check console log - assert 'Iteration 1 Status: success' in caplog.text - - # Check log file - log_entries = iteration_logger.get_log_entries() - assert len(log_entries) == 1 - assert log_entries[0]['iteration_number'] == 1 - assert log_entries[0]['status'] == 'success' - - -def test_log_multiple_iterations(iteration_logger): - """Test logging multiple iterations.""" - for i in range(3): - entry = IterationLogEntry( - iteration_number=i, - status='success', - performance_metrics={'accuracy': 0.9 + 0.01 * i} - ) +def test_filter_log_entries(iteration_logger): + """Test filtering log entries.""" + # Log multiple entries with different statuses and iteration numbers + entries = [ + IterationLogEntry(iteration_number=1, status='success', performance_metrics={'accuracy': 0.9}), + IterationLogEntry(iteration_number=2, status='failure', performance_metrics={'accuracy': 0.7}), + IterationLogEntry(iteration_number=3, status='success', performance_metrics={'accuracy': 0.95}), + ] + + for entry in entries: iteration_logger.log_iteration(entry) - log_entries = iteration_logger.get_log_entries() - assert len(log_entries) == 3 - - # Verify log entries - for i, entry in enumerate(log_entries): - assert entry['iteration_number'] == i - assert entry['status'] == 'success' - - -def test_log_iteration_with_error(iteration_logger): - """Test logging an iteration with error information.""" - entry = IterationLogEntry( - iteration_number=1, - status='failure', - error_info={'type': 'ValueError', 'message': 'Test error'} - ) - - iteration_logger.log_iteration(entry) - - log_entries = iteration_logger.get_log_entries() - assert len(log_entries) == 1 - assert log_entries[0]['error_info'] == {'type': 'ValueError', 'message': 'Test error'} - - -def test_log_entries_serialization(iteration_logger): - """Test JSON serialization of log entries.""" - entry = IterationLogEntry( - iteration_number=1, - metadata={'key': 'value'}, - performance_metrics={'accuracy': 0.95} - ) - - iteration_logger.log_iteration(entry) - - log_entries = iteration_logger.get_log_entries() - log_entry = log_entries[0] - - # Verify timestamp is serialized - assert 'timestamp' in log_entry - assert isinstance(log_entry['timestamp'], str) - - # Verify other fields - assert log_entry['iteration_number'] == 1 - assert log_entry['metadata'] == {'key': 'value'} - assert log_entry['performance_metrics'] == {'accuracy': 0.95} \ No newline at end of file + # Test filtering by status + success_entries = iteration_logger.filter_log_entries(status='success') + assert len(success_entries) == 2 + assert all(entry['status'] == 'success' for entry in success_entries) + + # Test filtering by minimum iteration + min_iteration_entries = iteration_logger.filter_log_entries(min_iteration=2) + assert len(min_iteration_entries) == 2 + assert all(entry['iteration_number'] >= 2 for entry in min_iteration_entries) + + # Test combined filtering + combined_filter = iteration_logger.filter_log_entries(status='success', min_iteration=2) + assert len(combined_filter) == 1 + assert combined_filter[0]['iteration_number'] == 3 + assert combined_filter[0]['status'] == 'success' \ No newline at end of file From 5e4beb537c630ed4488445ce34e005857740fb13 Mon Sep 17 00:00:00 2001 From: sopheakim Date: Sat, 5 Jul 2025 11:23:14 +0000 Subject: [PATCH 07/10] Add import to __init__.py for log_parser --- src/log_analysis/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/log_analysis/__init__.py b/src/log_analysis/__init__.py index 701e24e..289c1c2 100644 --- a/src/log_analysis/__init__.py +++ b/src/log_analysis/__init__.py @@ -1,3 +1 @@ -from .log_parser import LogParser - -__all__ = ['LogParser'] \ No newline at end of file +from .log_parser import LogParser \ No newline at end of file From e537a47a2b1de07e2253a5f345befd9cd537b9e8 Mon Sep 17 00:00:00 2001 From: sopheakim Date: Sat, 5 Jul 2025 11:23:49 +0000 Subject: [PATCH 08/10] Update analyze_performance to correctly handle performance scores --- src/log_analysis/log_parser.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/log_analysis/log_parser.py b/src/log_analysis/log_parser.py index 6ba602f..49233d1 100644 --- a/src/log_analysis/log_parser.py +++ b/src/log_analysis/log_parser.py @@ -75,20 +75,27 @@ def analyze_performance(self, log_entries: List[Dict[str, Any]]) -> Dict[str, An return {} # Extract performance-related metrics + performance_scores = [] + for entry in log_entries: + # Check for performance_score in both top-level and nested locations + score = entry.get('performance_score', 0) + if isinstance(score, (int, float)): + performance_scores.append(score) + performance_metrics = { 'total_iterations': len(log_entries), 'start_time': log_entries[0].get('timestamp'), 'end_time': log_entries[-1].get('timestamp'), - 'error_rate': sum(1 for entry in log_entries if entry.get('status') == 'error') / len(log_entries), - 'performance_scores': [entry.get('performance_metrics', {}).get('performance_score', 0) for entry in log_entries] + 'error_rate': sum(1 for entry in log_entries if entry.get('status') == 'error') / len(log_entries) } - # Calculate additional metrics - if performance_metrics['performance_scores']: + # Calculate performance metrics only if scores exist + if performance_scores: performance_metrics.update({ - 'avg_performance': sum(performance_metrics['performance_scores']) / len(performance_metrics['performance_scores']), - 'max_performance': max(performance_metrics['performance_scores']), - 'min_performance': min(performance_metrics['performance_scores']) + 'performance_scores': performance_scores, + 'avg_performance': sum(performance_scores) / len(performance_scores), + 'max_performance': max(performance_scores), + 'min_performance': min(performance_scores) }) return performance_metrics From 557c59fd90e7d507c81e8bb74aef6f26d5f362b9 Mon Sep 17 00:00:00 2001 From: sopheakim Date: Sat, 5 Jul 2025 11:24:10 +0000 Subject: [PATCH 09/10] Add test_filter_log_entries method and fixture for iteration logger --- tests/test_iteration_logger.py | 108 +++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/test_iteration_logger.py b/tests/test_iteration_logger.py index d17fedf..d9da343 100644 --- a/tests/test_iteration_logger.py +++ b/tests/test_iteration_logger.py @@ -1,3 +1,111 @@ +import os +import json +import pytest +import logging +from datetime import datetime +from src.iteration_logger import IterationLogger, IterationLogEntry + + +@pytest.fixture +def iteration_logger(tmp_path): + """Create a temporary IterationLogger for testing.""" + log_dir = str(tmp_path) + return IterationLogger(log_dir=log_dir) + + +def test_iteration_log_entry_creation(): + """Test creating an IterationLogEntry.""" + entry = IterationLogEntry( + iteration_number=1, + metadata={'model': 'test_model'}, + performance_metrics={'accuracy': 0.95} + ) + + assert entry.iteration_number == 1 + assert entry.metadata == {'model': 'test_model'} + assert entry.performance_metrics == {'accuracy': 0.95} + assert isinstance(entry.timestamp, datetime) + + +def test_log_iteration(iteration_logger, caplog): + """Test logging an iteration entry.""" + caplog.set_level(logging.INFO) + + entry = IterationLogEntry( + iteration_number=1, + status='success', + performance_metrics={'accuracy': 0.95} + ) + + iteration_logger.log_iteration(entry) + + # Check console log + assert 'Iteration 1 Status: success' in caplog.text + + # Check log file + log_entries = iteration_logger.get_log_entries() + assert len(log_entries) == 1 + assert log_entries[0]['iteration_number'] == 1 + assert log_entries[0]['status'] == 'success' + + +def test_log_multiple_iterations(iteration_logger): + """Test logging multiple iterations.""" + for i in range(3): + entry = IterationLogEntry( + iteration_number=i, + status='success', + performance_metrics={'accuracy': 0.9 + 0.01 * i} + ) + iteration_logger.log_iteration(entry) + + log_entries = iteration_logger.get_log_entries() + assert len(log_entries) == 3 + + # Verify log entries + for i, entry in enumerate(log_entries): + assert entry['iteration_number'] == i + assert entry['status'] == 'success' + + +def test_log_iteration_with_error(iteration_logger): + """Test logging an iteration with error information.""" + entry = IterationLogEntry( + iteration_number=1, + status='failure', + error_info={'type': 'ValueError', 'message': 'Test error'} + ) + + iteration_logger.log_iteration(entry) + + log_entries = iteration_logger.get_log_entries() + assert len(log_entries) == 1 + assert log_entries[0]['error_info'] == {'type': 'ValueError', 'message': 'Test error'} + + +def test_log_entries_serialization(iteration_logger): + """Test JSON serialization of log entries.""" + entry = IterationLogEntry( + iteration_number=1, + metadata={'key': 'value'}, + performance_metrics={'accuracy': 0.95} + ) + + iteration_logger.log_iteration(entry) + + log_entries = iteration_logger.get_log_entries() + log_entry = log_entries[0] + + # Verify timestamp is serialized + assert 'timestamp' in log_entry + assert isinstance(log_entry['timestamp'], str) + + # Verify other fields + assert log_entry['iteration_number'] == 1 + assert log_entry['metadata'] == {'key': 'value'} + assert log_entry['performance_metrics'] == {'accuracy': 0.95} + + def test_filter_log_entries(iteration_logger): """Test filtering log entries.""" # Log multiple entries with different statuses and iteration numbers From dc156a055dd625bcef89171a0d4b4b9faa9b81f8 Mon Sep 17 00:00:00 2001 From: sopheakim Date: Sat, 5 Jul 2025 11:24:37 +0000 Subject: [PATCH 10/10] Add filter_log_entries method to IterationLogger --- src/iteration_logger.py | 151 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/src/iteration_logger.py b/src/iteration_logger.py index 88e8056..3fcd644 100644 --- a/src/iteration_logger.py +++ b/src/iteration_logger.py @@ -1,3 +1,154 @@ +import logging +from typing import Dict, Any, Optional, List +from dataclasses import dataclass, asdict, field +from datetime import datetime +import json +import os + + +@dataclass +class IterationLogEntry: + """ + Structured log entry for a single ALP iteration. + + Attributes: + iteration_number (int): Unique identifier for the iteration + timestamp (datetime): Timestamp of the iteration + metadata (Dict[str, Any]): Additional metadata about the iteration + performance_metrics (Dict[str, float]): Performance metrics for the iteration + error_info (Optional[Dict[str, Any]]): Error information if an error occurred + status (str): Status of the iteration (success, failure, warning) + """ + iteration_number: int + timestamp: datetime = field(default_factory=datetime.now) + metadata: Dict[str, Any] = field(default_factory=dict) + performance_metrics: Dict[str, float] = field(default_factory=dict) + error_info: Optional[Dict[str, Any]] = None + status: str = 'pending' + + def to_dict(self) -> Dict[str, Any]: + """ + Convert log entry to a dictionary for JSON serialization. + + Returns: + Dict[str, Any]: Serializable dictionary representation of the log entry + """ + entry_dict = asdict(self) + entry_dict['timestamp'] = self.timestamp.isoformat() + return entry_dict + + +class IterationLogger: + """ + Utility class for managing and writing log entries for ALP iterations. + + Supports file-based and console logging with configurable log levels. + """ + def __init__( + self, + log_dir: str = 'logs', + log_file: str = 'iteration_log.json', + console_log_level: int = logging.INFO + ): + """ + Initialize the IterationLogger. + + Args: + log_dir (str): Directory to store log files + log_file (str): Name of the log file + console_log_level (int): Logging level for console output + """ + self.log_dir = log_dir + self.log_file = log_file + + # Create logs directory if it doesn't exist + os.makedirs(log_dir, exist_ok=True) + + # Full path for log file + self.log_path = os.path.join(log_dir, log_file) + + # Configure console logging + logging.basicConfig( + level=console_log_level, + format='%(asctime)s - %(levelname)s: %(message)s' + ) + self.logger = logging.getLogger(__name__) + + def log_iteration(self, entry: IterationLogEntry) -> None: + """ + Log an iteration entry to file and console. + + Args: + entry (IterationLogEntry): Iteration log entry to record + """ + try: + # Log to console + self._log_to_console(entry) + + # Append to JSON log file + self._append_to_log_file(entry) + except Exception as e: + self.logger.error(f"Error logging iteration: {e}") + + def _log_to_console(self, entry: IterationLogEntry) -> None: + """ + Log iteration details to console based on status. + + Args: + entry (IterationLogEntry): Iteration log entry + """ + log_method = { + 'success': self.logger.info, + 'failure': self.logger.error, + 'warning': self.logger.warning, + 'pending': self.logger.debug + }.get(entry.status, self.logger.info) + + log_method( + f"Iteration {entry.iteration_number} " + f"Status: {entry.status} " + f"Metrics: {entry.performance_metrics}" + ) + + def _append_to_log_file(self, entry: IterationLogEntry) -> None: + """ + Append iteration log entry to JSON log file. + + Args: + entry (IterationLogEntry): Iteration log entry + """ + try: + # Read existing log entries or initialize empty list + log_entries = [] + if os.path.exists(self.log_path): + with open(self.log_path, 'r') as f: + log_entries = json.load(f) + + # Append new entry + log_entries.append(entry.to_dict()) + + # Write updated log entries + with open(self.log_path, 'w') as f: + json.dump(log_entries, f, indent=2) + except Exception as e: + self.logger.error(f"Failed to write log entry: {e}") + + def get_log_entries(self) -> list: + """ + Retrieve all log entries from the log file. + + Returns: + list: List of log entries + """ + try: + if os.path.exists(self.log_path): + with open(self.log_path, 'r') as f: + return json.load(f) + return [] + except Exception as e: + self.logger.error(f"Error reading log entries: {e}") + return [] + def filter_log_entries(self, status: Optional[str] = None, min_iteration: Optional[int] = None) -> List[Dict[str, Any]]: """ Filter log entries based on status and minimum iteration number.