Loading noctua/config/constants.py +12 −25 Original line number Diff line number Diff line Loading @@ -113,13 +113,10 @@ image_number = {v: k for k, v in image_state.items()} # Directories ############ DATA_FOLDER_NAME = "data" LOG_DIRECTORY_NAME = "log" fits_folder = "fits" focus_folder = "focus" # data_folder = "data" # log_folder = "log" DATA_FOLDER = "data" LOG_FOLDER = "log" FITS_FOLDER = "fits" FOCUS_FOLDER = "focus" dir_type = { 0: "dark", Loading @@ -131,21 +128,6 @@ dir_type = { # Reversing dir_type dir_number = {v: k for k, v in dir_type.items()} ############ # Logging ############ # Log file naming LOG_FILE_EXTENSION = ".log" LATEST_LOG_FILENAME = "latest" # Rotation settings (used by logger.py) LOG_ROTATION_TIME_H = 12 # Midday (hour in UTC) LOG_ROTATION_TIME_M = 0 # Midday (minute in UTC) LOG_ROTATION_TIME_S = 0 # Midday (second in UTC) LOG_ROTATION_INTERVAL_DAYS = 1 # Rotate daily LOG_BACKUP_COUNT = 0 # 0 means keep all backup files (infinite) ############ # FITS ############ Loading @@ -154,6 +136,11 @@ LOG_BACKUP_COUNT = 0 # 0 means keep all backup files (infinite) imagetyp = "IMAGETYP" # 'Light' dateobs = "DATE-OBS" # '2021-12-18T05:09:56.163' FILE_PREFIX = "OARPAF." ext = ".fits" # File extension focus_ext = ".foc" ############ # EXTENSIONS ############ FILE_PREFIX = "OARPAF" FITS_EXT = "fits" # File extension FOCUS_EXT = "foc" LOG_EXT = "log" noctua/sequencer.py +2 −2 Original line number Diff line number Diff line Loading @@ -143,9 +143,9 @@ class Sequencer(): full_module_name = f"noctua.templates.{template_name}" try: tplmodule = importlib.import_module(full_module_name) except ModuleNotFoundError: except ModuleNotFoundError as e: log.error( f"SEQUENCER: Template module '{full_module_name}' not found.") f"SEQUENCER: Template module '{full_module_name}' not found: {e}") self.error.append( f"Template module '{full_module_name}' not found.") continue Loading noctua/templates/testlamp.py +1 −1 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ from time import sleep # Third-party modules from templates.basetemplate import BaseTemplate from .basetemplate import BaseTemplate # Other templates from ..config.constants import on_off Loading noctua/templates/testoutput.py +1 −1 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ from astropy.time import Time # Other templates from ..config.constants import on_off from ..directory.structure import foc_path from ..utils.structure import foc_path # from devices import lamp, light from ..utils.logger import log from .basetemplate import BaseTemplate Loading noctua/utils/logger.py +43 −155 Original line number Diff line number Diff line #!/usr/bin/env python3 # -*- coding: utf-8 -*- #!/usr/bin/env python """Custom format log""" # System modules import logging import logging.handlers import sys from pathlib import Path from datetime import datetime, time import os # Will be used if log_path only returns a directory # Third-party modules # Custom modules from ..config.constants import ( LOG_FILE_EXTENSION, LATEST_LOG_FILENAME, LOG_ROTATION_TIME_H, LOG_ROTATION_TIME_M, LOG_ROTATION_TIME_S, LOG_ROTATION_INTERVAL_DAYS, LOG_BACKUP_COUNT ) from .structure import get_log_dir_path, get_dated_log_filepath, get_latest_log_filepath LOG_DIR = get_log_dir_path() # Creates directory if it doesn't exist LATEST_LOG_FILEPATH = get_latest_log_filepath() log = None # Global logger instance class ColorizingFormatter(logging.Formatter): """ A custom logging formatter that adds ANSI color codes to log level names for console output. """ # Color codes BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) # Log level to color mapping Using bold for emphasis LEVEL_COLORS = { logging.DEBUG: f"\x1b[3{MAGENTA};1m", # Bold Magenta logging.INFO: f"\x1b[3{GREEN};1m", # Bold Green logging.WARNING: f"\x1b[3{YELLOW};1m", # Bold Yellow logging.ERROR: f"\x1b[3{RED};1m", # Bold Red logging.CRITICAL: f"\x1b[4{RED};1m", # Bold Red with Red Background (or just very bold red) } RESET_SEQ = "\x1b[0m" # Reset all attributes def __init__(self, fmt=None, datefmt=None, style='%', validate=True, *, defaults=None): super().__init__(fmt, datefmt, style, validate, defaults=defaults) # For UTC timestamps if needed by the base Formatter self.converter = lambda *args: datetime.utcnow().timetuple() def format(self, record): s = super().format(record) levelname_str = record.levelname color = self.LEVEL_COLORS.get(record.levelno) if color: padded_levelname = f"{levelname_str:<8}" if padded_levelname in s: colored_levelname = f"{color}{padded_levelname}{self.RESET_SEQ}" s = s.replace(padded_levelname, colored_levelname, 1) # Replace only the first occurrence else: # Fallback if padded version not found, try raw (less likely to match in typical formats) if levelname_str in s: colored_levelname = f"{color}{levelname_str}{self.RESET_SEQ}" s = s.replace(levelname_str, colored_levelname, 1) return s def setup_logger(name="noctua_logger", log_level=logging.DEBUG): global log if log: return log logger_instance = logging.getLogger(name) logger_instance.setLevel(log_level) if logger_instance.hasHandlers(): logger_instance.handlers.clear() log_format_str = "%(asctime)s.%(msecs)03d | %(levelname)-8s | %(message)s (%(module)s.%(funcName)s:%(lineno)d)" formatter = logging.Formatter(log_format_str, datefmt="%Y-%m-%d %H:%M:%S") # Ensure timestamps are in UTC formatter.converter = lambda *args: datetime.utcnow().timetuple() console_handler = logging.StreamHandler(sys.stderr) formatter = ColorizingFormatter(log_format_str, datefmt="%Y-%m-%d %H:%M:%S") console_handler.setFormatter(formatter) logger_instance.addHandler(console_handler) # --- TimedRotatingFileHandler for LATEST log --- # Rotate at midday UTC rotation_time_utc = time( hour=LOG_ROTATION_TIME_H, minute=LOG_ROTATION_TIME_M, second=LOG_ROTATION_TIME_S, tzinfo=None from loguru import logger import datetime from .structure import log_path def mylog(): "logger function" logger.remove() # Remove default handler to prevent duplicate console logs time_fmt = "{time:YYYY-MM-DD HH:mm:ss.SSSSSS!UTC} " level_fmt = "<level>{level: <8}</level> " message_fmt = "| {message} " stack_fmt = "<bold>({module}.{function}:{line})</bold>" fmt = time_fmt + level_fmt + message_fmt + stack_fmt # On standard output logger.add( sys.stderr, format=fmt, level="DEBUG" ) rotating_file_handler = logging.handlers.TimedRotatingFileHandler( filename=LATEST_LOG_FILEPATH, when="midnight", # Base rotation type (daily) atTime=rotation_time_utc, # Specific time for rotation interval=LOG_ROTATION_INTERVAL_DAYS, # Interval (1 for daily) backupCount=LOG_BACKUP_COUNT, # 0 means keep all backups encoding='utf-8', utc=True full_log_file_path = log_path() logger.add( full_log_file_path, format=fmt, colorize=True, # For file logs rotation="16:19", # Local time # retention="7 days", # Old logs level="DEBUG" ) def custom_namer_astronomical(default_name): try: # Example: '/.../OARPAF.latest.log.2023-04-16_12-00-00' (if rotated with atTime) base_filepath = Path(default_name) suffix = base_filepath.suffix # .log or or .2023-04-16_12-00-00 # Remove base filename and first dot date_str_part = base_filepath.name.replace(LATEST_LOG_FILEPATH.name + '.', '') date_str_part = date_str_part.split('_')[0] # Takes YYYY-MM-DD part if HH-MM-SS is appended rotation_calendar_date = datetime.strptime(date_str_part, "%Y-%m-%d").date() astronomical_date_of_log = rotation_calendar_date - timedelta(days=1) astro_date_str = astronomical_date_of_log.strftime("%Y-%m-%d") return str(get_dated_log_filepath(astro_date_str)) except ValueError: # Fallback if date parsing fails log.warning(f"Could not parse date from default rotated log name: {default_name}. Using default.") return default_name def simpler_custom_namer(default_name): base_filepath = Path(default_name) # Extract the date part from the suffix # Suffix is ".YYYY-MM-DD_HH-MM-SS" name_parts = base_filepath.name.split(LATEST_LOG_FILEPATH.name + '.') if len(name_parts) > 1: timestamp_suffix = name_parts[1] date_str = timestamp_suffix.split('_')[0] # Get the YYYY-MM-DD part try: rotation_event_date = datetime.strptime(date_str, "%Y-%m-%d").date() # The log contains data for the astronomical night *before* this rotation event date. log_file_astro_date = rotation_event_date - timedelta(days=1) final_date_str = log_file_astro_date.strftime("%Y-%m-%d") # Construct the desired filename using structure.py for consistency return str(get_dated_log_filepath(final_date_str)) except ValueError: pass # Fall through to default if parsing fails # Fallback to a cleaned-up version of the default name if parsing is tricky dir_name = base_filepath.parent original_plus_date = base_filepath.name.replace(LOG_FILE_EXTENSION, '') # Remove .log # original_plus_date is like OARPAF.latest.YYYY-MM-DD_HH-MM-SS cleaned_name = original_plus_date.replace(LATEST_LOG_FILENAME + '.', '') # Becomes OARPAF.YYYY-MM-DD... return str(dir_name / (cleaned_name + LOG_FILE_EXTENSION)) rotating_file_handler.namer = simpler_custom_namer # Use the simpler namer rotating_file_handler.setFormatter(formatter) logger_instance.addHandler(rotating_file_handler) log = logger_instance log.info(f"Logger '{name}' configured. Current logs to: {LATEST_LOG_FILEPATH}") log.info(f"Rotated daily at {rotation_time_utc} UTC. Backup count: {'Infinite' if LOG_BACKUP_COUNT == 0 else LOG_BACKUP_COUNT}.") return log # Initialize logger on module import from datetime import timedelta # Ensure timedelta is imported for namer log = setup_logger() # Custom levels logger.level("DEBUG", color="<magenta><bold>") logger.level("INFO", color="<green><bold>") if __name__ == "__main__": return logger # This ensures mylog() is called only once. log = mylog() def main(): """Main function""" # log is already initialized globally log.debug("This is a debug message.") log.info("This is an info message.") log.warning("This is a warning message.") log.error("This is an error message.") log.critical("This is a critical message.") if __name__ == "__main__": main() Loading
noctua/config/constants.py +12 −25 Original line number Diff line number Diff line Loading @@ -113,13 +113,10 @@ image_number = {v: k for k, v in image_state.items()} # Directories ############ DATA_FOLDER_NAME = "data" LOG_DIRECTORY_NAME = "log" fits_folder = "fits" focus_folder = "focus" # data_folder = "data" # log_folder = "log" DATA_FOLDER = "data" LOG_FOLDER = "log" FITS_FOLDER = "fits" FOCUS_FOLDER = "focus" dir_type = { 0: "dark", Loading @@ -131,21 +128,6 @@ dir_type = { # Reversing dir_type dir_number = {v: k for k, v in dir_type.items()} ############ # Logging ############ # Log file naming LOG_FILE_EXTENSION = ".log" LATEST_LOG_FILENAME = "latest" # Rotation settings (used by logger.py) LOG_ROTATION_TIME_H = 12 # Midday (hour in UTC) LOG_ROTATION_TIME_M = 0 # Midday (minute in UTC) LOG_ROTATION_TIME_S = 0 # Midday (second in UTC) LOG_ROTATION_INTERVAL_DAYS = 1 # Rotate daily LOG_BACKUP_COUNT = 0 # 0 means keep all backup files (infinite) ############ # FITS ############ Loading @@ -154,6 +136,11 @@ LOG_BACKUP_COUNT = 0 # 0 means keep all backup files (infinite) imagetyp = "IMAGETYP" # 'Light' dateobs = "DATE-OBS" # '2021-12-18T05:09:56.163' FILE_PREFIX = "OARPAF." ext = ".fits" # File extension focus_ext = ".foc" ############ # EXTENSIONS ############ FILE_PREFIX = "OARPAF" FITS_EXT = "fits" # File extension FOCUS_EXT = "foc" LOG_EXT = "log"
noctua/sequencer.py +2 −2 Original line number Diff line number Diff line Loading @@ -143,9 +143,9 @@ class Sequencer(): full_module_name = f"noctua.templates.{template_name}" try: tplmodule = importlib.import_module(full_module_name) except ModuleNotFoundError: except ModuleNotFoundError as e: log.error( f"SEQUENCER: Template module '{full_module_name}' not found.") f"SEQUENCER: Template module '{full_module_name}' not found: {e}") self.error.append( f"Template module '{full_module_name}' not found.") continue Loading
noctua/templates/testlamp.py +1 −1 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ from time import sleep # Third-party modules from templates.basetemplate import BaseTemplate from .basetemplate import BaseTemplate # Other templates from ..config.constants import on_off Loading
noctua/templates/testoutput.py +1 −1 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ from astropy.time import Time # Other templates from ..config.constants import on_off from ..directory.structure import foc_path from ..utils.structure import foc_path # from devices import lamp, light from ..utils.logger import log from .basetemplate import BaseTemplate Loading
noctua/utils/logger.py +43 −155 Original line number Diff line number Diff line #!/usr/bin/env python3 # -*- coding: utf-8 -*- #!/usr/bin/env python """Custom format log""" # System modules import logging import logging.handlers import sys from pathlib import Path from datetime import datetime, time import os # Will be used if log_path only returns a directory # Third-party modules # Custom modules from ..config.constants import ( LOG_FILE_EXTENSION, LATEST_LOG_FILENAME, LOG_ROTATION_TIME_H, LOG_ROTATION_TIME_M, LOG_ROTATION_TIME_S, LOG_ROTATION_INTERVAL_DAYS, LOG_BACKUP_COUNT ) from .structure import get_log_dir_path, get_dated_log_filepath, get_latest_log_filepath LOG_DIR = get_log_dir_path() # Creates directory if it doesn't exist LATEST_LOG_FILEPATH = get_latest_log_filepath() log = None # Global logger instance class ColorizingFormatter(logging.Formatter): """ A custom logging formatter that adds ANSI color codes to log level names for console output. """ # Color codes BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) # Log level to color mapping Using bold for emphasis LEVEL_COLORS = { logging.DEBUG: f"\x1b[3{MAGENTA};1m", # Bold Magenta logging.INFO: f"\x1b[3{GREEN};1m", # Bold Green logging.WARNING: f"\x1b[3{YELLOW};1m", # Bold Yellow logging.ERROR: f"\x1b[3{RED};1m", # Bold Red logging.CRITICAL: f"\x1b[4{RED};1m", # Bold Red with Red Background (or just very bold red) } RESET_SEQ = "\x1b[0m" # Reset all attributes def __init__(self, fmt=None, datefmt=None, style='%', validate=True, *, defaults=None): super().__init__(fmt, datefmt, style, validate, defaults=defaults) # For UTC timestamps if needed by the base Formatter self.converter = lambda *args: datetime.utcnow().timetuple() def format(self, record): s = super().format(record) levelname_str = record.levelname color = self.LEVEL_COLORS.get(record.levelno) if color: padded_levelname = f"{levelname_str:<8}" if padded_levelname in s: colored_levelname = f"{color}{padded_levelname}{self.RESET_SEQ}" s = s.replace(padded_levelname, colored_levelname, 1) # Replace only the first occurrence else: # Fallback if padded version not found, try raw (less likely to match in typical formats) if levelname_str in s: colored_levelname = f"{color}{levelname_str}{self.RESET_SEQ}" s = s.replace(levelname_str, colored_levelname, 1) return s def setup_logger(name="noctua_logger", log_level=logging.DEBUG): global log if log: return log logger_instance = logging.getLogger(name) logger_instance.setLevel(log_level) if logger_instance.hasHandlers(): logger_instance.handlers.clear() log_format_str = "%(asctime)s.%(msecs)03d | %(levelname)-8s | %(message)s (%(module)s.%(funcName)s:%(lineno)d)" formatter = logging.Formatter(log_format_str, datefmt="%Y-%m-%d %H:%M:%S") # Ensure timestamps are in UTC formatter.converter = lambda *args: datetime.utcnow().timetuple() console_handler = logging.StreamHandler(sys.stderr) formatter = ColorizingFormatter(log_format_str, datefmt="%Y-%m-%d %H:%M:%S") console_handler.setFormatter(formatter) logger_instance.addHandler(console_handler) # --- TimedRotatingFileHandler for LATEST log --- # Rotate at midday UTC rotation_time_utc = time( hour=LOG_ROTATION_TIME_H, minute=LOG_ROTATION_TIME_M, second=LOG_ROTATION_TIME_S, tzinfo=None from loguru import logger import datetime from .structure import log_path def mylog(): "logger function" logger.remove() # Remove default handler to prevent duplicate console logs time_fmt = "{time:YYYY-MM-DD HH:mm:ss.SSSSSS!UTC} " level_fmt = "<level>{level: <8}</level> " message_fmt = "| {message} " stack_fmt = "<bold>({module}.{function}:{line})</bold>" fmt = time_fmt + level_fmt + message_fmt + stack_fmt # On standard output logger.add( sys.stderr, format=fmt, level="DEBUG" ) rotating_file_handler = logging.handlers.TimedRotatingFileHandler( filename=LATEST_LOG_FILEPATH, when="midnight", # Base rotation type (daily) atTime=rotation_time_utc, # Specific time for rotation interval=LOG_ROTATION_INTERVAL_DAYS, # Interval (1 for daily) backupCount=LOG_BACKUP_COUNT, # 0 means keep all backups encoding='utf-8', utc=True full_log_file_path = log_path() logger.add( full_log_file_path, format=fmt, colorize=True, # For file logs rotation="16:19", # Local time # retention="7 days", # Old logs level="DEBUG" ) def custom_namer_astronomical(default_name): try: # Example: '/.../OARPAF.latest.log.2023-04-16_12-00-00' (if rotated with atTime) base_filepath = Path(default_name) suffix = base_filepath.suffix # .log or or .2023-04-16_12-00-00 # Remove base filename and first dot date_str_part = base_filepath.name.replace(LATEST_LOG_FILEPATH.name + '.', '') date_str_part = date_str_part.split('_')[0] # Takes YYYY-MM-DD part if HH-MM-SS is appended rotation_calendar_date = datetime.strptime(date_str_part, "%Y-%m-%d").date() astronomical_date_of_log = rotation_calendar_date - timedelta(days=1) astro_date_str = astronomical_date_of_log.strftime("%Y-%m-%d") return str(get_dated_log_filepath(astro_date_str)) except ValueError: # Fallback if date parsing fails log.warning(f"Could not parse date from default rotated log name: {default_name}. Using default.") return default_name def simpler_custom_namer(default_name): base_filepath = Path(default_name) # Extract the date part from the suffix # Suffix is ".YYYY-MM-DD_HH-MM-SS" name_parts = base_filepath.name.split(LATEST_LOG_FILEPATH.name + '.') if len(name_parts) > 1: timestamp_suffix = name_parts[1] date_str = timestamp_suffix.split('_')[0] # Get the YYYY-MM-DD part try: rotation_event_date = datetime.strptime(date_str, "%Y-%m-%d").date() # The log contains data for the astronomical night *before* this rotation event date. log_file_astro_date = rotation_event_date - timedelta(days=1) final_date_str = log_file_astro_date.strftime("%Y-%m-%d") # Construct the desired filename using structure.py for consistency return str(get_dated_log_filepath(final_date_str)) except ValueError: pass # Fall through to default if parsing fails # Fallback to a cleaned-up version of the default name if parsing is tricky dir_name = base_filepath.parent original_plus_date = base_filepath.name.replace(LOG_FILE_EXTENSION, '') # Remove .log # original_plus_date is like OARPAF.latest.YYYY-MM-DD_HH-MM-SS cleaned_name = original_plus_date.replace(LATEST_LOG_FILENAME + '.', '') # Becomes OARPAF.YYYY-MM-DD... return str(dir_name / (cleaned_name + LOG_FILE_EXTENSION)) rotating_file_handler.namer = simpler_custom_namer # Use the simpler namer rotating_file_handler.setFormatter(formatter) logger_instance.addHandler(rotating_file_handler) log = logger_instance log.info(f"Logger '{name}' configured. Current logs to: {LATEST_LOG_FILEPATH}") log.info(f"Rotated daily at {rotation_time_utc} UTC. Backup count: {'Infinite' if LOG_BACKUP_COUNT == 0 else LOG_BACKUP_COUNT}.") return log # Initialize logger on module import from datetime import timedelta # Ensure timedelta is imported for namer log = setup_logger() # Custom levels logger.level("DEBUG", color="<magenta><bold>") logger.level("INFO", color="<green><bold>") if __name__ == "__main__": return logger # This ensures mylog() is called only once. log = mylog() def main(): """Main function""" # log is already initialized globally log.debug("This is a debug message.") log.info("This is an info message.") log.warning("This is a warning message.") log.error("This is an error message.") log.critical("This is a critical message.") if __name__ == "__main__": main()