Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions datamint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions datamint/utils/logging_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions docs/source/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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_<timestamp>_<pid>.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
===================

Expand Down
Loading