Repository: hynek/structlog

Issue:
"exception" is a possible log level from processors.add_log_level since 23.3.0
structlog.processors.add_log_level used to report the level of "exception" events as "error", but since 23.3.0 with certain configurations it's possible to get "exception" instead:

```sh
$ pip3 install structlog==23.2.0 && python3 main.py
{'exc_info': True, 'event': 'test', '_record': <LogRecord: root, 40, 223, "{...}">, '_from_structlog': True, 'level': 'error'}

$ pip3 install structlog==23.3.0 && python3 main.py
{'exc_info': True, 'event': 'test', '_record': <LogRecord: root, 40, 217, "{...}">, '_from_structlog': True, 'level': 'exception'}
```

I wonder if this is intentional, since it's not very clearly listed in the changelog. There is "stdlib: structlog.stdlib.BoundLogger.exception()'s handling ofLogRecord.exc_info is now set consistent with logging." but I don't think this change falls into that category.

It seems to be reliant on `wrapper_class=structlog.stdlib.BoundLogger`.

Example code:
```python
import logging.config
import structlog

structlog.configure(
    processors=[
        structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
    ],
    logger_factory=structlog.stdlib.LoggerFactory(),
    wrapper_class=structlog.stdlib.BoundLogger,
)

logging_config = {
    "version": 1,
    "formatters": {
        "json": {
            "()": structlog.stdlib.ProcessorFormatter,
            "processors": [
                structlog.processors.add_log_level,
            ],
        }
    },
    "handlers": {
        "json": {
            "class": "logging.StreamHandler",
            "formatter": "json",
        },
    },
    "root": {
        "handlers": ["json"],
    },
}
logging.config.dictConfig(logging_config)
```

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5dd033e..85fb3e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,10 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
 - The lazy logger proxy returned by `structlog.get_logger()` now returns its initial values when asked for context.
   When asked for context before binding for the first time, it returned an empty dictionary in 23.3.0.
 
+- The displayed level name when using `structlog.stdlib.BoundLogger.exception()` is `"error"` instead of `"exception"`.
+  Fixes regression in 23.3.0.
+  [#584](https://github.com/hynek/structlog/issues/584)
+
 - Don't ignore the `width` argument of `RichTracebackFormatter`.
   [#587](https://github.com/hynek/structlog/issues/587)
 
diff --git a/src/structlog/_log_levels.py b/src/structlog/_log_levels.py
index bb7e67b..df1e81a 100644
--- a/src/structlog/_log_levels.py
+++ b/src/structlog/_log_levels.py
@@ -60,10 +60,15 @@ def add_log_level(
     .. versionchanged:: 20.2.0
        Importable from `structlog.processors` (additionally to
        `structlog.stdlib`).
+    .. versionchanged:: 24.1.0
+       Added mapping from "exception" to "error"
     """
     if method_name == "warn":
         # The stdlib has an alias
         method_name = "warning"
+    elif method_name == "exception":
+        # exception("") method is the same as error("", exc_info=True)
+        method_name = "error"
 
     event_dict["level"] = method_name
 


Test patch:
diff --git a/tests/test_stdlib.py b/tests/test_stdlib.py
index f1ae444..1c690f1 100644
--- a/tests/test_stdlib.py
+++ b/tests/test_stdlib.py
@@ -488,13 +488,16 @@ class TestAddLogLevel:
 
         assert "error" == event_dict["level"]
 
-    def test_log_level_alias_normalized(self):
+    @pytest.mark.parametrize(
+        ("alias", "normalized"), [("warn", "warning"), ("exception", "error")]
+    )
+    def test_log_level_alias_normalized(self, alias, normalized):
         """
         The normalized name of the log level is added to the event dict.
         """
-        event_dict = add_log_level(None, "warn", {})
+        event_dict = add_log_level(None, alias, {})
 
-        assert "warning" == event_dict["level"]
+        assert normalized == event_dict["level"]
 
 
 @pytest.fixture(name="make_log_record")