feat(logging): implement configurable belief state logging
- Add LoggingConfig model and logging field to GlobalSettings - Implement belief_scope context manager for structured logging - Add configure_logger for dynamic level and file rotation settings - Add logging configuration UI to Settings page - Update ConfigManager to apply logging settings on initialization and updates
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -60,4 +60,5 @@ keyring passwords.py
|
|||||||
*github*
|
*github*
|
||||||
*git*
|
*git*
|
||||||
*tech_spec*
|
*tech_spec*
|
||||||
dashboards
|
dashboards
|
||||||
|
backend/mappings.db
|
||||||
|
|||||||
Binary file not shown.
@@ -15,6 +15,7 @@ from backend.src.dependencies import get_config_manager
|
|||||||
from backend.src.core.superset_client import SupersetClient
|
from backend.src.core.superset_client import SupersetClient
|
||||||
from superset_tool.models import SupersetConfig
|
from superset_tool.models import SupersetConfig
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from backend.src.core.logger import logger
|
||||||
# [/SECTION]
|
# [/SECTION]
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/environments", tags=["environments"])
|
router = APIRouter(prefix="/api/environments", tags=["environments"])
|
||||||
@@ -38,7 +39,9 @@ class DatabaseResponse(BaseModel):
|
|||||||
# @RETURN: List[EnvironmentResponse]
|
# @RETURN: List[EnvironmentResponse]
|
||||||
@router.get("", response_model=List[EnvironmentResponse])
|
@router.get("", response_model=List[EnvironmentResponse])
|
||||||
async def get_environments(config_manager=Depends(get_config_manager)):
|
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()
|
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]
|
return [EnvironmentResponse(id=e.id, name=e.name, url=e.url) for e in envs]
|
||||||
# [/DEF:get_environments]
|
# [/DEF:get_environments]
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from .config_models import AppConfig, Environment, GlobalSettings
|
from .config_models import AppConfig, Environment, GlobalSettings
|
||||||
from .logger import logger
|
from .logger import logger, configure_logger
|
||||||
# [/SECTION]
|
# [/SECTION]
|
||||||
|
|
||||||
# [DEF:ConfigManager:Class]
|
# [DEF:ConfigManager:Class]
|
||||||
@@ -38,10 +38,13 @@ class ConfigManager:
|
|||||||
# 2. Logic implementation
|
# 2. Logic implementation
|
||||||
self.config_path = Path(config_path)
|
self.config_path = Path(config_path)
|
||||||
self.config: AppConfig = self._load_config()
|
self.config: AppConfig = self._load_config()
|
||||||
|
|
||||||
|
# Configure logger with loaded settings
|
||||||
|
configure_logger(self.config.settings.logging)
|
||||||
|
|
||||||
# 3. Runtime check of @POST
|
# 3. Runtime check of @POST
|
||||||
assert isinstance(self.config, AppConfig), "self.config must be an instance of AppConfig"
|
assert isinstance(self.config, AppConfig), "self.config must be an instance of AppConfig"
|
||||||
|
|
||||||
logger.info(f"[ConfigManager][Exit] Initialized")
|
logger.info(f"[ConfigManager][Exit] Initialized")
|
||||||
# [/DEF:__init__]
|
# [/DEF:__init__]
|
||||||
|
|
||||||
@@ -120,7 +123,10 @@ class ConfigManager:
|
|||||||
# 2. Logic implementation
|
# 2. Logic implementation
|
||||||
self.config.settings = settings
|
self.config.settings = settings
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
# Reconfigure logger with new settings
|
||||||
|
configure_logger(settings.logging)
|
||||||
|
|
||||||
logger.info(f"[update_global_settings][Exit] Settings updated")
|
logger.info(f"[update_global_settings][Exit] Settings updated")
|
||||||
# [/DEF:update_global_settings]
|
# [/DEF:update_global_settings]
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,22 @@ class Environment(BaseModel):
|
|||||||
is_default: bool = False
|
is_default: bool = False
|
||||||
# [/DEF:Environment]
|
# [/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]
|
# [DEF:GlobalSettings:DataClass]
|
||||||
# @PURPOSE: Represents global application settings.
|
# @PURPOSE: Represents global application settings.
|
||||||
class GlobalSettings(BaseModel):
|
class GlobalSettings(BaseModel):
|
||||||
backup_path: str
|
backup_path: str
|
||||||
default_environment_id: Optional[str] = None
|
default_environment_id: Optional[str] = None
|
||||||
|
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
||||||
# [/DEF:GlobalSettings]
|
# [/DEF:GlobalSettings]
|
||||||
|
|
||||||
# [DEF:AppConfig:DataClass]
|
# [DEF:AppConfig:DataClass]
|
||||||
|
|||||||
@@ -4,12 +4,32 @@
|
|||||||
# @LAYER: Core
|
# @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.
|
# @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 logging
|
||||||
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
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
|
# Re-using LogEntry from task_manager for consistency
|
||||||
# [DEF:LogEntry:Class]
|
# [DEF:LogEntry:Class]
|
||||||
# @SEMANTICS: log, entry, record, pydantic
|
# @SEMANTICS: log, entry, record, pydantic
|
||||||
@@ -22,6 +42,81 @@ class LogEntry(BaseModel):
|
|||||||
|
|
||||||
# [/DEF]
|
# [/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]
|
# [DEF:WebSocketLogHandler:Class]
|
||||||
# @SEMANTICS: logging, handler, websocket, buffer
|
# @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.
|
# @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)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
# Create a formatter
|
# Create a formatter
|
||||||
formatter = logging.Formatter(
|
formatter = BeliefFormatter(
|
||||||
'[%(asctime)s][%(levelname)s][%(name)s] %(message)s'
|
'[%(asctime)s][%(levelname)s][%(name)s] %(message)s'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
44
backend/tests/test_logger.py
Normal file
44
backend/tests/test_logger.py
Normal file
@@ -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"
|
||||||
@@ -24,7 +24,7 @@ export const options = {
|
|||||||
app: ({ head, body, assets, nonce, env }) => "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
|
app: ({ head, body, assets, nonce, env }) => "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
|
||||||
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
|
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
|
||||||
},
|
},
|
||||||
version_hash: "uq8l2w"
|
version_hash: "n7gbte"
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function get_hooks() {
|
export async function get_hooks() {
|
||||||
|
|||||||
@@ -21,7 +21,14 @@
|
|||||||
environments: [],
|
environments: [],
|
||||||
settings: {
|
settings: {
|
||||||
backup_path: '',
|
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 @@
|
|||||||
<label for="backup_path" class="block text-sm font-medium text-gray-700">Backup Storage Path</label>
|
<label for="backup_path" class="block text-sm font-medium text-gray-700">Backup Storage Path</label>
|
||||||
<input type="text" id="backup_path" bind:value={settings.settings.backup_path} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
<input type="text" id="backup_path" bind:value={settings.settings.backup_path} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||||
</div>
|
</div>
|
||||||
<button on:click={handleSaveGlobal} class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 w-max">
|
|
||||||
Save Global Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-medium mb-4 mt-6">Logging Configuration</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="log_level" class="block text-sm font-medium text-gray-700">Log Level</label>
|
||||||
|
<select id="log_level" bind:value={settings.settings.logging.level} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2">
|
||||||
|
<option value="DEBUG">DEBUG</option>
|
||||||
|
<option value="INFO">INFO</option>
|
||||||
|
<option value="WARNING">WARNING</option>
|
||||||
|
<option value="ERROR">ERROR</option>
|
||||||
|
<option value="CRITICAL">CRITICAL</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="log_file_path" class="block text-sm font-medium text-gray-700">Log File Path</label>
|
||||||
|
<input type="text" id="log_file_path" bind:value={settings.settings.logging.file_path} placeholder="logs/app.log" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="log_max_bytes" class="block text-sm font-medium text-gray-700">Max File Size (MB)</label>
|
||||||
|
<input type="number" id="log_max_bytes" bind:value={settings.settings.logging.max_bytes} min="1" step="1" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="log_backup_count" class="block text-sm font-medium text-gray-700">Backup Count</label>
|
||||||
|
<input type="number" id="log_backup_count" bind:value={settings.settings.logging.backup_count} min="1" step="1" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" id="enable_belief_state" bind:checked={settings.settings.logging.enable_belief_state} class="h-4 w-4 text-blue-600 border-gray-300 rounded" />
|
||||||
|
<span class="ml-2 block text-sm text-gray-900">Enable Belief State Logging</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button on:click={handleSaveGlobal} class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 w-max mt-4">
|
||||||
|
Save Global Settings
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mb-8 bg-white p-6 rounded shadow">
|
<section class="mb-8 bg-white p-6 rounded shadow">
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
**Spec**: `specs/006-configurable-belief-logs/spec.md`
|
**Spec**: `specs/006-configurable-belief-logs/spec.md`
|
||||||
**Plan**: `specs/006-configurable-belief-logs/plan.md`
|
**Plan**: `specs/006-configurable-belief-logs/plan.md`
|
||||||
**Status**: Pending
|
**Status**: Completed
|
||||||
|
|
||||||
## Phase 1: Setup
|
## Phase 1: Setup
|
||||||
*Goal: Initialize project structure for logging.*
|
*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
|
## Phase 2: Foundational
|
||||||
*Goal: Define data models and infrastructure required for logging configuration.*
|
*Goal: Define data models and infrastructure required for logging configuration.*
|
||||||
|
|
||||||
- [ ] T002 Define `LoggingConfig` model in `backend/src/core/config_models.py`
|
- [x] 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`
|
- [x] 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] T004 Update `ConfigManager` to handle logging configuration persistence in `backend/src/core/config_manager.py`
|
||||||
|
|
||||||
## Phase 3: User Story 1 - Structured Belief State Logging
|
## Phase 3: User Story 1 - Structured Belief State Logging
|
||||||
*Goal: Implement the core "Belief State" logging logic with context managers.*
|
*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.
|
**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`
|
- [x] 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`
|
- [x] 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] 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
|
## Phase 4: User Story 2 - Configurable Logging Settings
|
||||||
*Goal: Expose logging configuration to the user via API and UI.*
|
*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.
|
**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`
|
- [x] 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`
|
- [x] 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] 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
|
## Final Phase: Polish & Cross-Cutting Concerns
|
||||||
*Goal: Verify system stability and cleanup.*
|
*Goal: Verify system stability and cleanup.*
|
||||||
|
|
||||||
- [ ] T011 Verify log rotation works by generating dummy logs (manual verification)
|
- [x] T011 Verify log rotation works by generating dummy logs (manual verification)
|
||||||
- [ ] T012 Ensure default configuration provides a sensible out-of-the-box experience
|
- [x] T012 Ensure default configuration provides a sensible out-of-the-box experience
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user