From c7661f2aebcd79873248d90992dc3e7a48901f11 Mon Sep 17 00:00:00 2001 From: vertighel Date: Fri, 30 May 2025 17:00:21 +0200 Subject: [PATCH] loguru again --- noctua/config/constants.py | 37 ++---- noctua/sequencer.py | 4 +- noctua/templates/testlamp.py | 2 +- noctua/templates/testoutput.py | 2 +- noctua/utils/logger.py | 198 +++++++-------------------------- noctua/utils/structure.py | 53 ++++----- pyproject.toml | 1 + 7 files changed, 79 insertions(+), 218 deletions(-) diff --git a/noctua/config/constants.py b/noctua/config/constants.py index 3829f67..c05fb8b 100644 --- a/noctua/config/constants.py +++ b/noctua/config/constants.py @@ -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", @@ -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 ############ @@ -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" diff --git a/noctua/sequencer.py b/noctua/sequencer.py index 249b2c3..6a68fbc 100755 --- a/noctua/sequencer.py +++ b/noctua/sequencer.py @@ -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 diff --git a/noctua/templates/testlamp.py b/noctua/templates/testlamp.py index d1b8a25..8087902 100644 --- a/noctua/templates/testlamp.py +++ b/noctua/templates/testlamp.py @@ -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 diff --git a/noctua/templates/testoutput.py b/noctua/templates/testoutput.py index 61d6d71..15f29ec 100644 --- a/noctua/templates/testoutput.py +++ b/noctua/templates/testoutput.py @@ -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 diff --git a/noctua/utils/logger.py b/noctua/utils/logger.py index 07163bd..832f9c9 100644 --- a/noctua/utils/logger.py +++ b/noctua/utils/logger.py @@ -1,176 +1,64 @@ -#!/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: <8} " + message_fmt = "| {message} " + stack_fmt = "({module}.{function}:{line})" + 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 + # Custom levels + logger.level("DEBUG", color="") + logger.level("INFO", color="") - # 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)) + return logger - 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 +# This ensures mylog() is called only once. +log = mylog() -# Initialize logger on module import -from datetime import timedelta # Ensure timedelta is imported for namer -log = setup_logger() +def main(): + """Main function""" -if __name__ == "__main__": + # 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() diff --git a/noctua/utils/structure.py b/noctua/utils/structure.py index 3e0e260..3b895e6 100644 --- a/noctua/utils/structure.py +++ b/noctua/utils/structure.py @@ -14,16 +14,13 @@ from astropy.io import fits from astropy.time import Time # Custom modules -from ..config.constants import ( - DATA_FOLDER_NAME, LOG_DIRECTORY_NAME, - FILE_PREFIX, LOG_FILE_EXTENSION, LATEST_LOG_FILENAME, - dateobs, dir_type, ext, fits_folder, - frame_number, imagetyp, focus_folder, focus_ext -) +from ..config.constants import ( DATA_FOLDER, LOG_FOLDER, FITS_FOLDER, + FOCUS_FOLDER, FILE_PREFIX, dateobs, + dir_type, frame_number, imagetyp, + FITS_EXT, FOCUS_EXT, LOG_EXT ) PROJECT_ROOT = Path(__file__).parent.parent.parent - def date_folder(): """Create a date folder string based on astronomical convention (changes at midday UTC). @@ -59,7 +56,7 @@ def fits_path(header, dry=False): Create a fits file path where the file will be stored """ - root = PROJECT_ROOT / DATA_FOLDER_NAME / fits_folder + root = PROJECT_ROOT / DATA_FOLDER / FITS_FOLDER date_str = date_folder() date_path_part = Path(date_str) @@ -72,34 +69,23 @@ def fits_path(header, dry=False): return path -def get_log_dir_path(dry=False): +def log_path(dry=False): """ Returns the Path object for the log directory. Creates it if it doesn't exist (unless dry=True). """ - log_dir = PROJECT_ROOT / DATA_FOLDER_NAME / LOG_DIRECTORY_NAME - if not dry: - log_dir.mkdir(parents=True, exist_ok=True) - return log_dir - -def get_dated_log_filepath(date_str, dry=False): - """ - Constructs a path for an expected date-stamped (rotated) log - file. date_str: A string like "YYYY-MM-DD" - """ - log_dir = get_log_dir_path(dry=dry) - # Filename: OARPAF.YYYY-MM-DD.log - log_filename = f"{FILE_PREFIX}{date_str}{LOG_FILE_EXTENSION}" - return log_dir / log_filename + path = PROJECT_ROOT / DATA_FOLDER / LOG_FOLDER + + if not dry: + path.mkdir(parents=True, exist_ok=True) -def get_latest_log_filepath(dry=False): - """ - Constructs the path for the "latest" log file. - """ - log_dir = get_log_dir_path(dry=dry) - latest_filename = f"{FILE_PREFIX}{LATEST_LOG_FILENAME}{LOG_FILE_EXTENSION}" - return log_dir / latest_filename + # OARPAF.YYYY-MM-DD.foc + outfile = f"{FILE_PREFIX}.{LOG_EXT}" + outpath = path / outfile + return outpath + + return outpath def foc_path(timestamp, dry=False): @@ -107,14 +93,13 @@ def foc_path(timestamp, dry=False): Create the focus output text file name and its path """ - root = PROJECT_ROOT / DATA_FOLDER_NAME / focus_folder - path = root + path = PROJECT_ROOT / DATA_FOLDER / FOCUS_FOLDER if not dry: path.mkdir(parents=True, exist_ok=True) # OARPAF.YYYY-MM-DD.foc - outfile = f"{FILE_PREFIX}{timestamp}{focus_ext}" + outfile = f"{FILE_PREFIX}.{timestamp}.{FOCUS_EXT}" outpath = path / outfile return outpath @@ -131,7 +116,7 @@ def save_filename(infile_path_str): date_obs_str = header[dateobs] # DATE-OBS from FITS header name_for_file = Time(date_obs_str).isot - outfile_name = f"{FILE_PREFIX}{name_for_file}{ext}" + outfile_name = f"{FILE_PREFIX}.{name_for_file}.{FITS_EXT}" outfile = Path(outfile_name) outdir = fits_path(header) # This already creates the directory diff --git a/pyproject.toml b/pyproject.toml index 445eb77..49fd104 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ dependencies = [ "requests", "astropy", + "loguru", "gnuplotlib", ## If focus.py "pyvantagepro", # If meteo.py ] -- GitLab