From 254f6885f9064b3d899ecb820c8166c343a8ca98 Mon Sep 17 00:00:00 2001 From: luandalmazo Date: Mon, 3 Aug 2026 10:58:17 -0300 Subject: [PATCH] add log to a file feature --- datamint/__init__.py | 3 ++ datamint/utils/logging_utils.py | 57 +++++++++++++++++++++++++++++++++ docs/source/getting_started.rst | 12 +++++++ 3 files changed, 72 insertions(+) diff --git a/datamint/__init__.py b/datamint/__init__.py index c88b952c..e1399afd 100644 --- a/datamint/__init__.py +++ b/datamint/__init__.py @@ -4,6 +4,9 @@ import importlib.metadata from typing import TYPE_CHECKING +from .utils.logging_utils import setup_file_logging_if_enabled + +setup_file_logging_if_enabled() if TYPE_CHECKING: from .api.client import Api # New modular datasets diff --git a/datamint/utils/logging_utils.py b/datamint/utils/logging_utils.py index 39bba36d..4fa1d5c4 100644 --- a/datamint/utils/logging_utils.py +++ b/datamint/utils/logging_utils.py @@ -10,9 +10,13 @@ from rich.traceback import Traceback import yaml import importlib +import datetime _LOGGER = logging.getLogger(__name__) +_FILE_HANDLER_MARKER = '_datamint_file_handler' +_FALSY_ENV_VALUES = {'0', 'false', 'no'} + class ConditionalRichHandler(RichHandler): """ @@ -59,6 +63,59 @@ def load_cmdline_logging_config(): _LOGGER.exception(e) logging.basicConfig(level=logging.INFO) + # dictConfig replaces the handlers of the loggers it configures, so the file + # handler must be (re)attached after it runs. + setup_file_logging_if_enabled() + + +def _file_logging_enabled() -> bool: + value = os.environ.get('DATAMINT_LOG_FILE') + if value is None: + return True + return value.strip().lower() not in _FALSY_ENV_VALUES + + +def _has_file_handler(logger: logging.Logger) -> bool: + return any(getattr(handler, _FILE_HANDLER_MARKER, False) for handler in logger.handlers) + + +def setup_file_logging_if_enabled(): + """ + Attaches a per-run file handler to the 'datamint' and 'user_logger' loggers, + writing to ./.log/, unless DATAMINT_LOG_FILE is explicitly disabled. + """ + try: + if not _file_logging_enabled(): + return + + datamint_logger = logging.getLogger('datamint') + user_logger = logging.getLogger('user_logger') + + if _has_file_handler(datamint_logger) or _has_file_handler(user_logger): + return + + log_dir = os.path.join(os.getcwd(), '.log') + os.makedirs(log_dir, exist_ok=True) + + timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"datamint_{timestamp}_{os.getpid()}.log" + filepath = os.path.join(log_dir, filename) + + handler = logging.FileHandler(filepath) + handler.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter( + "%(asctime)s - %(levelname)s - %(filename)s:%(funcName)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + )) + setattr(handler, _FILE_HANDLER_MARKER, True) + + for logger in (datamint_logger, user_logger): + logger.addHandler(handler) + if logger.getEffectiveLevel() > logging.INFO: + logger.setLevel(logging.INFO) + except Exception as e: + print(f"Warning: Could not set up file logging: {e}") + LEVELS_MAPPING = { DEBUG: None, diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 80dccd60..4e9db199 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -49,6 +49,18 @@ Don't have your own data yet? ``datamint init`` can populate the project with a public example dataset instead — see :ref:`command_line_tools` for details, or run ``datamint example --help`` directly. +Logging to a file +=================== + +Every ``datamint`` command (and the SDK itself, when imported in your own scripts) +writes an INFO-level log to a file under ``./.log/`` in the current working +directory, one file per run, named ``datamint__.log``. This is meant +as a support/debug artifact you can attach when reporting an issue, it captures +warnings, errors, and full tracebacks regardless of how verbose the console output is. + +This is on by default. To turn it off, set the ``DATAMINT_LOG_FILE`` environment +variable to ``0``, ``false``, or ``no``: + Your first API call ===================