"""
Configure logging for this session.
.. rubric:: Public
.. autosummary::
    ~configure_logging
.. rubric:: Internal
.. autosummary::
    ~_setup_console_logger
    ~_setup_file_logger
    ~_setup_ipython_logger
    ~_setup_module_logging
.. seealso:: https://blueskyproject.io/bluesky/main/debugging.html
"""
import logging
import logging.handlers
import os
import pathlib
BYTE = 1
kB = 1024 * BYTE
MB = 1024 * kB
BRIEF_DATE = "%a-%H:%M:%S"
BRIEF_FORMAT = "%(levelname)-.1s %(asctime)s.%(msecs)03d: %(message)s"
DEFAULT_CONFIG_FILE = (
    pathlib.Path(__file__).parent.parent / "demo_instrument" / "configs" / "logging.yml"
)
# Add your custom logging level at the top-level, before configure_logging()
[docs]
def addLoggingLevel(levelName, levelNum, methodName=None):
    """
    Comprehensively adds a new logging level to the `logging` module and the
    currently configured logging class.
    `levelName` becomes an attribute of the `logging` module with the value
    `levelNum`. `methodName` becomes a convenience method for both `logging`
    itself and the class returned by `logging.getLoggerClass()` (usually just
    `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
    used.
    To avoid accidental clobberings of existing attributes, this method will
    raise an `AttributeError` if the level name is already an attribute of the
    `logging` module or if the method name is already present
    Example
    -------
    >>> addLoggingLevel('TRACE', logging.INFO - 5)
    >>> logging.getLogger(__name__).setLevel("TEST")
    >>> logging.getLogger(__name__).test('that worked')
    >>> logging.test('so did this')
    >>> logging.TEST
    5
    """
    if not methodName:
        methodName = levelName.lower()
    if hasattr(logging, levelName):
        raise AttributeError("{} already defined in logging module".format(levelName))
    if hasattr(logging, methodName):
        raise AttributeError("{} already defined in logging module".format(methodName))
    if hasattr(logging.getLoggerClass(), methodName):
        raise AttributeError("{} already defined in logger class".format(methodName))
    # This method was inspired by the answers to Stack Overflow post
    # http://stackoverflow.com/q/2183233/2988730, especially
    # http://stackoverflow.com/a/13638084/2988730
    def logForLevel(self, message, *args, **kwargs):
        if self.isEnabledFor(levelNum):
            self._log(levelNum, message, args, **kwargs)
    def logToRoot(message, *args, **kwargs):
        logging.log(levelNum, message, *args, **kwargs)
    logging.addLevelName(levelNum, levelName)
    setattr(logging, levelName, levelNum)
    setattr(logging.getLoggerClass(), methodName, logForLevel)
    setattr(logging, methodName, logToRoot) 
addLoggingLevel("BSDEV", logging.INFO - 5)
[docs]
def _setup_console_logger(logger, cfg):
    """
    Reconfigure the root logger as configured by the user.
    We can't apply user configurations in ``configure_logging()`` above
    because the code to read the config file triggers initialization of
    the logging system.
    .. seealso:: https://docs.python.org/3/library/logging.html#logging.basicConfig
    """
    logging.basicConfig(
        encoding="utf-8",
        level=cfg["root_level"].upper(),
        format=cfg["log_format"],
        datefmt=cfg["date_format"],
        force=True,  # replace any previous setup
    )
    h = logger.handlers[0]
    h.setLevel(cfg["level"].upper()) 
[docs]
def _setup_file_logger(logger, cfg):
    """Record log messages in file(s)."""
    formatter = logging.Formatter(
        fmt=cfg["log_format"],
        datefmt=cfg["date_format"],
        style="%",
        validate=True,
    )
    formatter.default_msec_format = "%s.%03d"
    backupCount = cfg.get("backupCount", 9)
    maxBytes = cfg.get("maxBytes", 1 * MB)
    log_path = pathlib.Path(cfg.get("log_directory", ".logs")).resolve()
    if not log_path.exists():
        os.makedirs(str(log_path))
    file_name = log_path / cfg.get("log_filename_base", "logging.log")
    if maxBytes > 0 or backupCount > 0:
        backupCount = max(backupCount, 1)  # impose minimum standards
        maxBytes = max(maxBytes, 100 * kB)
        handler = logging.handlers.RotatingFileHandler(
            file_name,
            maxBytes=maxBytes,
            backupCount=backupCount,
        )
    else:
        handler = logging.FileHandler(file_name)
    handler.setFormatter(formatter)
    if cfg.get("rotate_on_startup", False):
        handler.doRollover()
    logger.addHandler(handler)
    logger.info("%s Bluesky Startup", "*" * 40)
    logger.bsdev(__file__)
    logger.bsdev("Log file: %s", file_name) 
[docs]
def _setup_ipython_logger(logger, cfg):
    """
    Internal: Log IPython console session In and Out to a file.
    See ``logrotate?`` int he IPython console for more information.
    """
    log_path = pathlib.Path(cfg.get("log_directory", ".logs")).resolve()
    try:
        from IPython import get_ipython
        # start logging console to file
        # https://ipython.org/ipython-doc/3/interactive/magics.html#magic-logstart
        _ipython = get_ipython()
        log_file = log_path / cfg.get("log_filename_base", "ipython_log.py")
        log_mode = cfg.get("log_mode", "rotate")
        options = cfg.get("options", "-o -t")
        if _ipython is not None:
            print(
                "\nBelow are the IPython logging settings for your session."
                "\nThese settings have no impact on your experiment.\n"
            )
            _ipython.run_line_magic("logstart", f"{options} {log_file} {log_mode}")
            if logger is not None:
                logger.bsdev("Console logging: %s", log_file)
    except Exception as exc:
        if logger is None:
            print(f"Could not setup console logging: {exc}")
        else:
            logger.exception("Could not setup console logging.") 
[docs]
def _setup_module_logging(cfg):
    """Internal: Set logging level for each named module."""
    for module, level in cfg.items():
        logging.getLogger(module).setLevel(level.upper())