diff --git a/src/datajoint/autopopulate.py b/src/datajoint/autopopulate.py index b40ebbda4..c02f18791 100644 --- a/src/datajoint/autopopulate.py +++ b/src/datajoint/autopopulate.py @@ -598,7 +598,7 @@ def _populate1( jobs.complete(key) return False - logger.debug(f"Making {key} -> {self.full_table_name}") + logger.jobs(f"Making {key} -> {self.full_table_name}") self.__class__._allow_insert = True try: @@ -629,7 +629,7 @@ def _populate1( exception=error.__class__.__name__, msg=": " + str(error) if str(error) else "", ) - logger.debug(f"Error making {key} -> {self.full_table_name} - {error_message}") + logger.jobs(f"Error making {key} -> {self.full_table_name} - {error_message}") if jobs is not None: jobs.error(key, error_message=error_message, error_stack=traceback.format_exc()) if not suppress_errors or isinstance(error, SystemExit): @@ -640,7 +640,7 @@ def _populate1( else: self.connection.commit_transaction() duration = time.time() - start_time - logger.debug(f"Success making {key} -> {self.full_table_name}") + logger.jobs(f"Success making {key} -> {self.full_table_name}") # Update hidden job metadata if table has the columns if self._has_job_metadata_attrs(): diff --git a/src/datajoint/logging.py b/src/datajoint/logging.py index b432e1a4b..280d08167 100644 --- a/src/datajoint/logging.py +++ b/src/datajoint/logging.py @@ -2,14 +2,44 @@ import os import sys +# Custom log level for job/populate status messages +# DEBUG (10) < JOBS (15) < INFO (20) < WARNING (30) < ERROR (40) +JOBS = 15 +logging.addLevelName(JOBS, "JOBS") + + +def jobs(self, message, *args, **kwargs): + """Log job status messages (make start/success/error).""" + if self.isEnabledFor(JOBS): + self._log(JOBS, message, args, **kwargs) + + +logging.Logger.jobs = jobs + logger = logging.getLogger(__name__.split(".")[0]) log_level = os.getenv("DJ_LOG_LEVEL", "info").upper() +log_stream = os.getenv("DJ_LOG_STREAM", "stdout").lower() + + +class LevelAwareFormatter(logging.Formatter): + """Format INFO messages cleanly, show level for warnings/errors and JOBS.""" + + def format(self, record): + timestamp = self.formatTime(record, "%Y-%m-%d %H:%M:%S") + if record.levelno >= logging.WARNING: + return f"[{timestamp}][{record.levelname}]: {record.getMessage()}" + elif record.levelno == JOBS: + return f"[{timestamp}][JOBS]: {record.getMessage()}" + else: + return f"[{timestamp}] {record.getMessage()}" -log_format = logging.Formatter("[%(asctime)s][%(levelname)s]: %(message)s") -stream_handler = logging.StreamHandler() # default handler -stream_handler.setFormatter(log_format) +# Select output stream: stdout (default, no red highlighting) or stderr +# Configurable via DJ_LOG_STREAM=stdout|stderr +output_stream = sys.stderr if log_stream == "stderr" else sys.stdout +stream_handler = logging.StreamHandler(output_stream) +stream_handler.setFormatter(LevelAwareFormatter()) logger.setLevel(level=log_level) logger.handlers = [stream_handler] diff --git a/src/datajoint/version.py b/src/datajoint/version.py index 068e6fe5c..acc17bb66 100644 --- a/src/datajoint/version.py +++ b/src/datajoint/version.py @@ -1,4 +1,4 @@ # version bump auto managed by Github Actions: # label_prs.yaml(prep), release.yaml(bump), post_release.yaml(edit) # manually set this version will be eventually overwritten by the above actions -__version__ = "2.0.0a25" +__version__ = "2.0.0a26"