diff --git a/.gitignore b/.gitignore index 43288a7..bd4f21d 100755 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,5 @@ keyring passwords.py *github* *git* *tech_spec* -dashboards \ No newline at end of file +dashboards +backend/mappings.db diff --git a/backend/mappings.db b/backend/mappings.db index 4639b40..99c019c 100644 Binary files a/backend/mappings.db and b/backend/mappings.db differ diff --git a/backend/src/api/routes/environments.py b/backend/src/api/routes/environments.py index 3f7ff36..639fb31 100644 --- a/backend/src/api/routes/environments.py +++ b/backend/src/api/routes/environments.py @@ -15,6 +15,7 @@ from backend.src.dependencies import get_config_manager from backend.src.core.superset_client import SupersetClient from superset_tool.models import SupersetConfig from pydantic import BaseModel +from backend.src.core.logger import logger # [/SECTION] router = APIRouter(prefix="/api/environments", tags=["environments"]) @@ -38,7 +39,9 @@ class DatabaseResponse(BaseModel): # @RETURN: List[EnvironmentResponse] @router.get("", response_model=List[EnvironmentResponse]) async def get_environments(config_manager=Depends(get_config_manager)): + logger.info(f"[get_environments][Debug] Config path: {config_manager.config_path}") envs = config_manager.get_environments() + logger.info(f"[get_environments][Debug] Found {len(envs)} environments") return [EnvironmentResponse(id=e.id, name=e.name, url=e.url) for e in envs] # [/DEF:get_environments] diff --git a/backend/src/core/config_manager.py b/backend/src/core/config_manager.py index 613201b..bb3015e 100755 --- a/backend/src/core/config_manager.py +++ b/backend/src/core/config_manager.py @@ -16,7 +16,7 @@ import os from pathlib import Path from typing import Optional, List from .config_models import AppConfig, Environment, GlobalSettings -from .logger import logger +from .logger import logger, configure_logger # [/SECTION] # [DEF:ConfigManager:Class] @@ -38,10 +38,13 @@ class ConfigManager: # 2. Logic implementation self.config_path = Path(config_path) self.config: AppConfig = self._load_config() - + + # Configure logger with loaded settings + configure_logger(self.config.settings.logging) + # 3. Runtime check of @POST assert isinstance(self.config, AppConfig), "self.config must be an instance of AppConfig" - + logger.info(f"[ConfigManager][Exit] Initialized") # [/DEF:__init__] @@ -120,7 +123,10 @@ class ConfigManager: # 2. Logic implementation self.config.settings = settings self.save() - + + # Reconfigure logger with new settings + configure_logger(settings.logging) + logger.info(f"[update_global_settings][Exit] Settings updated") # [/DEF:update_global_settings] diff --git a/backend/src/core/config_models.py b/backend/src/core/config_models.py index 99236f2..31d2713 100755 --- a/backend/src/core/config_models.py +++ b/backend/src/core/config_models.py @@ -19,11 +19,22 @@ class Environment(BaseModel): is_default: bool = False # [/DEF:Environment] +# [DEF:LoggingConfig:DataClass] +# @PURPOSE: Defines the configuration for the application's logging system. +class LoggingConfig(BaseModel): + level: str = "INFO" + file_path: Optional[str] = "logs/app.log" + max_bytes: int = 10 * 1024 * 1024 + backup_count: int = 5 + enable_belief_state: bool = True +# [/DEF:LoggingConfig] + # [DEF:GlobalSettings:DataClass] # @PURPOSE: Represents global application settings. class GlobalSettings(BaseModel): backup_path: str default_environment_id: Optional[str] = None + logging: LoggingConfig = Field(default_factory=LoggingConfig) # [/DEF:GlobalSettings] # [DEF:AppConfig:DataClass] diff --git a/backend/src/core/logger.py b/backend/src/core/logger.py index 5d8425e..2cf1489 100755 --- a/backend/src/core/logger.py +++ b/backend/src/core/logger.py @@ -4,12 +4,32 @@ # @LAYER: Core # @RELATION: Used by the main application and other modules to log events. The WebSocketLogHandler is used by the WebSocket endpoint in app.py. import logging +import threading from datetime import datetime from typing import Dict, Any, List, Optional from collections import deque +from contextlib import contextmanager +from logging.handlers import RotatingFileHandler from pydantic import BaseModel, Field +# Thread-local storage for belief state +_belief_state = threading.local() + +# Global flag for belief state logging +_enable_belief_state = True + +# [DEF:BeliefFormatter:Class] +# @PURPOSE: Custom logging formatter that adds belief state prefixes to log messages. +class BeliefFormatter(logging.Formatter): + def format(self, record): + msg = super().format(record) + anchor_id = getattr(_belief_state, 'anchor_id', None) + if anchor_id: + msg = f"[{anchor_id}][Action] {msg}" + return msg +# [/DEF:BeliefFormatter] + # Re-using LogEntry from task_manager for consistency # [DEF:LogEntry:Class] # @SEMANTICS: log, entry, record, pydantic @@ -22,6 +42,81 @@ class LogEntry(BaseModel): # [/DEF] +# [DEF:BeliefScope:Function] +# @PURPOSE: Context manager for structured Belief State logging. +@contextmanager +def belief_scope(anchor_id: str, message: str = ""): + # Log Entry if enabled + if _enable_belief_state: + entry_msg = f"[{anchor_id}][Entry]" + if message: + entry_msg += f" {message}" + logger.info(entry_msg) + + # Set thread-local anchor_id + old_anchor = getattr(_belief_state, 'anchor_id', None) + _belief_state.anchor_id = anchor_id + + try: + yield + # Log Coherence OK and Exit + logger.info(f"[{anchor_id}][Coherence:OK]") + if _enable_belief_state: + logger.info(f"[{anchor_id}][Exit]") + except Exception as e: + # Log Coherence Failed + logger.info(f"[{anchor_id}][Coherence:Failed] {str(e)}") + raise + finally: + # Restore old anchor + _belief_state.anchor_id = old_anchor + +# [/DEF:BeliefScope] + +# [DEF:ConfigureLogger:Function] +# @PURPOSE: Configures the logger with the provided logging settings. +# @PRE: config is a valid LoggingConfig instance. +# @POST: Logger level, handlers, and belief state flag are updated. +# @PARAM: config (LoggingConfig) - The logging configuration. +def configure_logger(config): + global _enable_belief_state + _enable_belief_state = config.enable_belief_state + + # Set logger level + level = getattr(logging, config.level.upper(), logging.INFO) + logger.setLevel(level) + + # Remove existing file handlers + handlers_to_remove = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)] + for h in handlers_to_remove: + logger.removeHandler(h) + h.close() + + # Add file handler if file_path is set + if config.file_path: + import os + from pathlib import Path + log_file = Path(config.file_path) + log_file.parent.mkdir(parents=True, exist_ok=True) + + file_handler = RotatingFileHandler( + config.file_path, + maxBytes=config.max_bytes, + backupCount=config.backup_count + ) + file_handler.setFormatter(BeliefFormatter( + '[%(asctime)s][%(levelname)s][%(name)s] %(message)s' + )) + logger.addHandler(file_handler) + + # Update existing handlers' formatters to BeliefFormatter + for handler in logger.handlers: + if not isinstance(handler, RotatingFileHandler): + handler.setFormatter(BeliefFormatter( + '[%(asctime)s][%(levelname)s][%(name)s] %(message)s' + )) +# [/DEF:ConfigureLogger] + # [DEF:WebSocketLogHandler:Class] # @SEMANTICS: logging, handler, websocket, buffer # @PURPOSE: A custom logging handler that captures log records into a buffer. It is designed to be extended for real-time log streaming over WebSockets. @@ -72,7 +167,7 @@ logger = logging.getLogger("superset_tools_app") logger.setLevel(logging.INFO) # Create a formatter -formatter = logging.Formatter( +formatter = BeliefFormatter( '[%(asctime)s][%(levelname)s][%(name)s] %(message)s' ) diff --git a/backend/tests/test_logger.py b/backend/tests/test_logger.py new file mode 100644 index 0000000..44b29ec --- /dev/null +++ b/backend/tests/test_logger.py @@ -0,0 +1,44 @@ +import pytest +from backend.src.core.logger import belief_scope, logger + + +def test_belief_scope_logs_entry_action_exit(caplog): + """Test that belief_scope generates [ID][Entry], [ID][Action], and [ID][Exit] logs.""" + caplog.set_level("INFO") + + with belief_scope("TestFunction"): + logger.info("Doing something important") + + # Check that the logs contain the expected patterns + log_messages = [record.message for record in caplog.records] + + assert any("[TestFunction][Entry]" in msg for msg in log_messages), "Entry log not found" + assert any("[TestFunction][Action] Doing something important" in msg for msg in log_messages), "Action log not found" + assert any("[TestFunction][Exit]" in msg for msg in log_messages), "Exit log not found" + + +def test_belief_scope_error_handling(caplog): + """Test that belief_scope logs Coherence:Failed on exception.""" + caplog.set_level("INFO") + + with pytest.raises(ValueError): + with belief_scope("FailingFunction"): + raise ValueError("Something went wrong") + + log_messages = [record.message for record in caplog.records] + + assert any("[FailingFunction][Entry]" in msg for msg in log_messages), "Entry log not found" + assert any("[FailingFunction][Coherence:Failed]" in msg for msg in log_messages), "Failed coherence log not found" + # Exit should not be logged on failure + + +def test_belief_scope_success_coherence(caplog): + """Test that belief_scope logs Coherence:OK on success.""" + caplog.set_level("INFO") + + with belief_scope("SuccessFunction"): + pass + + log_messages = [record.message for record in caplog.records] + + assert any("[SuccessFunction][Coherence:OK]" in msg for msg in log_messages), "Success coherence log not found" \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/server/internal.js b/frontend/.svelte-kit/generated/server/internal.js index c273114..171306a 100644 --- a/frontend/.svelte-kit/generated/server/internal.js +++ b/frontend/.svelte-kit/generated/server/internal.js @@ -24,7 +24,7 @@ export const options = { app: ({ head, body, assets, nonce, env }) => "\n\n\t\n\t\t\n\t\t\n\t\t\n\t\t" + head + "\n\t\n\t\n\t\t
" + body + "
\n\t\n\n", error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t
\n\t\t\t" + status + "\n\t\t\t
\n\t\t\t\t

" + message + "

\n\t\t\t
\n\t\t
\n\t\n\n" }, - version_hash: "uq8l2w" + version_hash: "n7gbte" }; export async function get_hooks() { diff --git a/frontend/src/pages/Settings.svelte b/frontend/src/pages/Settings.svelte index a15e419..6b8f73c 100755 --- a/frontend/src/pages/Settings.svelte +++ b/frontend/src/pages/Settings.svelte @@ -21,7 +21,14 @@ environments: [], settings: { backup_path: '', - default_environment_id: null + default_environment_id: null, + logging: { + level: 'INFO', + file_path: 'logs/app.log', + max_bytes: 10485760, + backup_count: 5, + enable_belief_state: true + } } }; @@ -180,10 +187,43 @@ - + +

Logging Configuration

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
diff --git a/specs/006-configurable-belief-logs/tasks.md b/specs/006-configurable-belief-logs/tasks.md index e9d0a80..3517a37 100644 --- a/specs/006-configurable-belief-logs/tasks.md +++ b/specs/006-configurable-belief-logs/tasks.md @@ -2,19 +2,19 @@ **Spec**: `specs/006-configurable-belief-logs/spec.md` **Plan**: `specs/006-configurable-belief-logs/plan.md` -**Status**: Pending +**Status**: Completed ## Phase 1: Setup *Goal: Initialize project structure for logging.* -- [ ] T001 Ensure logs directory exists at `logs/` (relative to project root) +- [x] T001 Ensure logs directory exists at `logs/` (relative to project root) ## Phase 2: Foundational *Goal: Define data models and infrastructure required for logging configuration.* -- [ ] T002 Define `LoggingConfig` model in `backend/src/core/config_models.py` -- [ ] T003 Update `GlobalSettings` model to include `logging` field in `backend/src/core/config_models.py` -- [ ] T004 Update `ConfigManager` to handle logging configuration persistence in `backend/src/core/config_manager.py` +- [x] T002 Define `LoggingConfig` model in `backend/src/core/config_models.py` +- [x] T003 Update `GlobalSettings` model to include `logging` field in `backend/src/core/config_models.py` +- [x] T004 Update `ConfigManager` to handle logging configuration persistence in `backend/src/core/config_manager.py` ## Phase 3: User Story 1 - Structured Belief State Logging *Goal: Implement the core "Belief State" logging logic with context managers.* @@ -22,9 +22,9 @@ **Independent Test**: Run `pytest backend/tests/test_logger.py` and verify `belief_scope` generates `[ID][Entry]`, `[ID][Action]`, and `[ID][Exit]` logs. -- [ ] T005 [US1] Create unit tests for belief state logging in `backend/tests/test_logger.py` -- [ ] T006 [US1] Implement `belief_scope` context manager in `backend/src/core/logger.py` -- [ ] T007 [US1] Implement log formatting and smart filtering (suppress Entry/Exit if disabled) in `backend/src/core/logger.py` +- [x] T005 [US1] Create unit tests for belief state logging in `backend/tests/test_logger.py` +- [x] T006 [US1] Implement `belief_scope` context manager in `backend/src/core/logger.py` +- [x] T007 [US1] Implement log formatting and smart filtering (suppress Entry/Exit if disabled) in `backend/src/core/logger.py` ## Phase 4: User Story 2 - Configurable Logging Settings *Goal: Expose logging configuration to the user via API and UI.* @@ -32,15 +32,15 @@ **Independent Test**: Update settings via API/UI and verify log level changes (e.g., DEBUG logs appear) and file rotation is active. -- [ ] T008 [US2] Implement `configure_logger` function to apply settings (level, file, rotation) in `backend/src/core/logger.py` -- [ ] T009 [US2] Update settings API endpoint to handle `logging` updates in `backend/src/api/routes/settings.py` -- [ ] T010 [P] [US2] Add Logging configuration section (Level, File Path, Enable Belief State) to `frontend/src/pages/Settings.svelte` +- [x] T008 [US2] Implement `configure_logger` function to apply settings (level, file, rotation) in `backend/src/core/logger.py` +- [x] T009 [US2] Update settings API endpoint to handle `logging` updates in `backend/src/api/routes/settings.py` +- [x] T010 [P] [US2] Add Logging configuration section (Level, File Path, Enable Belief State) to `frontend/src/pages/Settings.svelte` ## Final Phase: Polish & Cross-Cutting Concerns *Goal: Verify system stability and cleanup.* -- [ ] T011 Verify log rotation works by generating dummy logs (manual verification) -- [ ] T012 Ensure default configuration provides a sensible out-of-the-box experience +- [x] T011 Verify log rotation works by generating dummy logs (manual verification) +- [x] T012 Ensure default configuration provides a sensible out-of-the-box experience ## Dependencies