diff --git a/TODO.md b/TODO.md index df669ab..f50fceb 100644 --- a/TODO.md +++ b/TODO.md @@ -181,6 +181,7 @@ After completing a milestone, create a pull request with your changes for review - [x] Integrated evaluation metrics and plots into the Data Explorer page - [x] Implemented modeling page with model selection, training, cross-validation, and export functionality - [x] Added histogram, box plot, violin plot, and heatmap UI with export options +- [x] Added logging utilities and integrated logging across the app ## PR17: Robust Error Handling diff --git a/app.py b/app.py index ac2b112..3144964 100644 --- a/app.py +++ b/app.py @@ -2,6 +2,9 @@ import streamlit as st from utils import ui +from utils.logging import configure_logging + +configure_logging() st.set_page_config(page_title="PredictStream", layout="wide") diff --git a/pages/data_explorer.py b/pages/data_explorer.py index 0e80380..58681d9 100644 --- a/pages/data_explorer.py +++ b/pages/data_explorer.py @@ -6,6 +6,10 @@ from utils import eda from utils import ui from utils import components +import logging +from utils.logging import configure_logging + +configure_logging() @@ -47,6 +51,9 @@ def main() -> None: ) st.success(f"{name} loaded!") except (ValueError, TypeError) as exc: + logging.getLogger(__name__).error( + "Failed to load sample data %s: %s", name, exc + ) st.error(f"Failed to load sample data: {exc}") with st.expander("Help"): diff --git a/pages/modeling.py b/pages/modeling.py index c5edb30..f884bfe 100644 --- a/pages/modeling.py +++ b/pages/modeling.py @@ -10,6 +10,9 @@ import streamlit as st from utils import model, predict, ui +from utils.logging import configure_logging + +configure_logging() st.set_page_config(page_title="Modeling", layout="wide") diff --git a/pages/prediction.py b/pages/prediction.py index 004452b..1903c91 100644 --- a/pages/prediction.py +++ b/pages/prediction.py @@ -11,6 +11,9 @@ from utils import data as data_utils from utils import predict from utils import ui +from utils.logging import configure_logging + +configure_logging() st.set_page_config(page_title="Prediction", layout="wide") diff --git a/pages/report.py b/pages/report.py index 5e9d0f3..a1b7fcc 100644 --- a/pages/report.py +++ b/pages/report.py @@ -11,6 +11,9 @@ from utils import data as data_utils from utils import eda from utils import ui +from utils.logging import configure_logging + +configure_logging() st.set_page_config(page_title="Report", layout="wide") diff --git a/pages/time_series.py b/pages/time_series.py index 7090807..fe5518d 100644 --- a/pages/time_series.py +++ b/pages/time_series.py @@ -7,6 +7,9 @@ import streamlit as st from utils import ui, viz +from utils.logging import configure_logging + +configure_logging() st.set_page_config(page_title="Time Series", layout="wide") diff --git a/tests/test_logging_utils.py b/tests/test_logging_utils.py new file mode 100644 index 0000000..ff67b5a --- /dev/null +++ b/tests/test_logging_utils.py @@ -0,0 +1,10 @@ +import logging +from importlib import reload +from utils import logging as log_utils + + +def test_configure_logging_sets_level(): + reload(log_utils) + log_utils.configure_logging(level=logging.INFO) + assert logging.getLogger().level == logging.INFO + diff --git a/utils/__init__.py b/utils/__init__.py index 8d208cb..2668773 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -10,6 +10,7 @@ from . import predict from . import ui from . import transform +from . import logging __all__ = [ "config", @@ -22,4 +23,5 @@ "predict", "ui", "transform", + "logging", ] diff --git a/utils/components.py b/utils/components.py index e68167c..1f6f83b 100644 --- a/utils/components.py +++ b/utils/components.py @@ -4,6 +4,7 @@ from pathlib import Path import tempfile +import logging import pandas as pd import streamlit as st @@ -441,6 +442,9 @@ def classification_training_section(data: pd.DataFrame) -> None: ) except Exception as exc: # pragma: no cover - training can fail for many reasons progress.progress(0) + logging.getLogger(__name__).exception( + "Classification training failed: %s", exc + ) st.error(f"Classification training failed: {exc}") st.subheader("Detected Problem Type") @@ -637,6 +641,9 @@ def regression_training_section(data: pd.DataFrame) -> None: ) except Exception as exc: # pragma: no cover - training can fail for many reasons progress_r.progress(0) + logging.getLogger(__name__).exception( + "Regression training failed: %s", exc + ) st.error(f"Regression training failed: {exc}") if st.button("Compare Regression Models") and feature_cols_r: diff --git a/utils/data.py b/utils/data.py index 1652264..58d73d7 100644 --- a/utils/data.py +++ b/utils/data.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Any, Iterable +import logging MAX_UPLOAD_SIZE_MB = 100 @@ -37,7 +38,8 @@ def validate_file_size(file: Any, max_mb: int = MAX_UPLOAD_SIZE_MB) -> int: else: path = Path(getattr(file, "name")) size = path.stat().st_size - except OSError: + except OSError as exc: + logging.getLogger(__name__).warning("Could not determine file size: %s", exc) size = None if size is not None and size > limit: raise ValueError(f"File size {size} exceeds limit of {limit} bytes") @@ -60,6 +62,7 @@ def load_data(file: Any) -> pd.DataFrame: path = Path(str(file)) ext = path.suffix.lower() if not path.exists(): + logging.getLogger(__name__).error("Invalid file path: %s", path) raise ValueError("Invalid file path") try: @@ -68,7 +71,9 @@ def load_data(file: Any) -> pd.DataFrame: if ext in {".xls", ".xlsx"}: return pd.read_excel(file) except Exception as exc: # pragma: no cover - pass through + logging.getLogger(__name__).exception("Failed to read file: %s", exc) raise ValueError(f"Failed to read file: {exc}") from exc + logging.getLogger(__name__).error("Unsupported file type: %s", ext) raise ValueError(f"Unsupported file type: {ext}") @@ -104,9 +109,11 @@ def validate_file_type(file: Any, allowed_types: Iterable[str]) -> str: path = Path(str(file)) ext = path.suffix.lower() if not path.exists(): + logging.getLogger(__name__).error("Invalid file path: %s", path) raise ValueError("Invalid file path") if ext not in {f".{t.lstrip('.').lower()}" for t in allowed_types}: + logging.getLogger(__name__).error("Unsupported file type: %s", ext) raise ValueError(f"Unsupported file type: {ext}") return ext @@ -128,6 +135,7 @@ def process_uploaded_file( df = load_data(uploaded_file) df = convert_dtypes(df) except (ValueError, TypeError) as exc: # pragma: no cover - tested via wrapper + logging.getLogger(__name__).error("Failed to load uploaded file: %s", exc) st.error(f"Failed to load file: {exc}") return None st.session_state[session_key] = df diff --git a/utils/logging.py b/utils/logging.py new file mode 100644 index 0000000..493589a --- /dev/null +++ b/utils/logging.py @@ -0,0 +1,14 @@ +import logging + + +def configure_logging(level: int = logging.INFO) -> None: + """Configure root logger if not already configured.""" + root = logging.getLogger() + if not root.handlers: + logging.basicConfig( + level=level, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + ) + else: + root.setLevel(level) + diff --git a/utils/viz.py b/utils/viz.py index 7ff644f..f4ac71e 100644 --- a/utils/viz.py +++ b/utils/viz.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Optional +import logging import pandas as pd import plotly.express as px @@ -135,20 +136,26 @@ def export_figure(fig: object, path: Path) -> None: if isinstance(fig, go.Figure): fig.write_html(str(path)) else: + logging.getLogger(__name__).error("HTML export requires a Plotly figure") raise ValueError("HTML export requires a Plotly figure") elif ext in {".png", ".jpg", ".jpeg"}: if isinstance(fig, go.Figure): try: fig.write_image(str(path)) except ValueError as exc: + logging.getLogger(__name__).exception( + "Static image export failed: %s", exc + ) raise RuntimeError( "Static image export requires the kaleido package" ) from exc elif isinstance(fig, plt.Figure): fig.savefig(path) else: + logging.getLogger(__name__).error("Unsupported figure type for image export") raise ValueError("Unsupported figure type for image export") else: + logging.getLogger(__name__).error("Unsupported export format: %s", ext) raise ValueError(f"Unsupported export format: {ext}")