Repository: cta-observatory/ctapipe (python)

Issue:
Conflict between DIRAC python module and the ctapipe use of logging
Hi all, 
I faced to a conflict between the use of logging within ctapipe and the python DIRAC API. Indeed the `logging` module gives the possibility to modify its configuration, in particular we can add new log level to the module. It's exactly what the DIRAC python module does. 
The problem is that the `ctapipe.core.logging` module sets up the log for `ctapipe` but is expecting the initial configuration of the logging module. Indeed at https://github.com/cta-observatory/ctapipe/blob/8eaba747106bbb7ff35db020bed202d39a1fa4a1/src/ctapipe/core/logging.py#L29 the dictionary of log level name keywords is hard coded, I would suggest to write it with a more versatile way. 

Steps to reproduce the behavior:
1. create a `tool` which inherits from the `ctapipe.core.tool` main class, this tool will do nothing
2. overwrite the setup method and add the following line :`from DIRAC.Interfaces.API.Dirac import Dirac`
3. create the tool, then run the `tool.setup()` and then try to do a `tool.log.warning("hello")`

I guess that this bug could appear as well if you are not doing this within a `tool`, but it's with the `tool` that I personally discovered its.  

I get the following error showing that there is an error with  the color formatter of the log : 
```
Traceback (most recent call last):
  File "/home/ggroller/.conda/envs/nectarchain/lib/python3.11/logging/__init__.py", line 1110, in emit
    msg = self.format(record)
          ^^^^^^^^^^^^^^^^^^^
  File "/home/ggroller/.conda/envs/nectarchain/lib/python3.11/logging/__init__.py", line 953, in format
    return fmt.format(record)
           ^^^^^^^^^^^^^^^^^^
  File "/home/ggroller/.conda/envs/nectarchain/lib/python3.11/site-packages/ctapipe/core/logging.py", line 22, in format
    return s.replace(record.levelname, apply_colors(record.levelname))
                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ggroller/.conda/envs/nectarchain/lib/python3.11/site-packages/ctapipe/core/logging.py", line 38, in apply_colors
    levelname_color = color_seq % (30 + colors[levelname]) + levelname + reset_seq
                                        ~~~~~~^^^^^^^^^^^
KeyError: 'WARN'
```

To fix this issue, I found two methods : 
1. Add "WARN :  yellow" at https://github.com/cta-observatory/ctapipe/blob/8eaba747106bbb7ff35db020bed202d39a1fa4a1/src/ctapipe/core/logging.py#L29 
2. I include all my DIRAC imports to a context manager which is used  to reset the `logging` configuration as it was configured before the DIRAC imports. 







Pull request:
Improve logging colorization, fixes #2504

Patch:
diff --git a/docs/changes/2505.bugfix.rst b/docs/changes/2505.bugfix.rst
new file mode 100644
index 00000000..357b2084
--- /dev/null
+++ b/docs/changes/2505.bugfix.rst
@@ -0,0 +1,1 @@
+Fix colored logging in case of custom log levels being defined.
diff --git a/src/ctapipe/core/logging.py b/src/ctapipe/core/logging.py
index b9928fae..8879346d 100644
--- a/src/ctapipe/core/logging.py
+++ b/src/ctapipe/core/logging.py
@@ -2,12 +2,44 @@
 
 import logging
 from collections.abc import Mapping
+from enum import IntEnum
 
 DEFAULT_LOGGING_FORMAT = (
     "%(asctime)s %(levelname)s [%(name)s] (%(module)s.%(funcName)s): %(message)s"
 )
 
 
+class ANSIEscapes(IntEnum):
+    """
+    Enum of some ANSI style escape sequences used for logging.
+
+    See https://en.wikipedia.org/wiki/ANSI_escape_code
+    """
+
+    RESET = 0
+    BOLD = 1
+    FG_RED = 31
+    FG_GREEN = 32
+    FG_YELLOW = 33
+    FG_BLUE = 34
+    FG_MAGENTA = 35
+
+
+LEVEL_COLORS = {
+    "DEBUG": ANSIEscapes.FG_BLUE,
+    "INFO": ANSIEscapes.FG_GREEN,
+    "WARNING": ANSIEscapes.FG_YELLOW,
+    "ERROR": ANSIEscapes.FG_RED,
+    "CRITICAL": ANSIEscapes.FG_MAGENTA,
+}
+
+
+def add_ansi_display(string, *styles):
+    """Surround ``string`` by ANSI escape code styling."""
+    styles = ";".join(str(int(style)) for style in styles)
+    return f"\033[{styles}m{string}\033[{ANSIEscapes.RESET}m"
+
+
 class PlainFormatter(logging.Formatter):
     """Custom logging.Formatter used for file logging."""
 
@@ -23,20 +55,12 @@ class ColoredFormatter(logging.Formatter):
 
 def apply_colors(levelname: str):
     """Use ANSI escape sequences to add colors the levelname of log entries."""
-    _black, red, green, yellow, blue, magenta, _cyan, _white = range(8)
-    reset_seq = "\033[0m"
-    color_seq = "\033[1;%dm"
-    colors = {
-        "INFO": green,
-        "DEBUG": blue,
-        "WARNING": yellow,
-        "CRITICAL": magenta,
-        "ERROR": red,
-    }
+    color = LEVEL_COLORS.get(levelname)
 
-    levelname_color = color_seq % (30 + colors[levelname]) + levelname + reset_seq
+    if color is None:
+        return add_ansi_display(levelname, ANSIEscapes.BOLD)
 
-    return levelname_color
+    return add_ansi_display(levelname, ANSIEscapes.BOLD, color)
 
 
 def recursive_update(d1, d2, copy=False):


Test patch:
diff --git a/src/ctapipe/core/tests/test_logging.py b/src/ctapipe/core/tests/test_logging.py
new file mode 100644
index 00000000..29c5c92e
--- /dev/null
+++ b/src/ctapipe/core/tests/test_logging.py
@@ -0,0 +1,12 @@
+def test_colors():
+    """Test applying colors"""
+    from ctapipe.core.logging import apply_colors
+
+    assert apply_colors("DEBUG") == "\x1b[1;34mDEBUG\x1b[0m"
+    assert apply_colors("INFO") == "\x1b[1;32mINFO\x1b[0m"
+    assert apply_colors("WARNING") == "\x1b[1;33mWARNING\x1b[0m"
+    assert apply_colors("ERROR") == "\x1b[1;31mERROR\x1b[0m"
+    assert apply_colors("CRITICAL") == "\x1b[1;35mCRITICAL\x1b[0m"
+
+    # unknown log-level, regression test for #2504
+    assert apply_colors("FOO") == "\x1b[1mFOO\x1b[0m"