Repository: hynek/structlog (Python)

Issue:
`CallsiteParameterAdder` not pickleable
I am developing an application where I bind a collection of data to a logger and ultimately pass that logger into subprocess. For my application, it's desired to have call stack information from the  [`CallsiteParameterAdder`](https://github.com/hynek/structlog/blob/186d2eaf7a1d78cba940bd3e99277a538894a6f2/src/structlog/processors.py#L727-L863) ; however, I noticed that the serialization failed when passing a logger with this processor through the pipe (b/c of the lambda functions)

The following implementation works well; however, it would be nice to have native support or an example in the docs of how to make it pickleable (please disregard if this already exists and I just missed it). 

```
def _pathname(module, frame_info) -> Any:
    return frame_info.filename

def _filename(module, frame_info) -> Any:
    return os.path.basename(frame_info.filename)

def _module(module, frame_info) -> Any:
    return os.path.splitext(os.path.basename(frame_info.filename))[0]

def _func_name(module, frame_info) -> Any:
    return frame_info.function

def _lineno(module, frame_info) -> Any:
    return frame_info.lineno

def _thread(module, frame_info) -> Any:
    return threading.get_ident()

def _thread_name(module, frame_info) -> Any:
    return threading.current_thread().name

def _process(module, frame_info) -> Any:
    return os.getpid()

def _process_name(module, frame_info) -> Any:
    return get_processname()

class PickleableCallsiteParameterAdder(CallsiteParameterAdder):
    """
    Subclass of `CallsiteParameterAdder` that is pickleable.

    The original implementation is not pickleable because it uses lambda functions to define
    a handlers dict (see https://github.com/hynek/structlog/blob/186d2eaf7a1d78cba940bd3e99277a538894a6f2/src/structlog/processors.py#L767-L797).

    This subclass wraps the original, but redefines the lambda functions as top level functions
    """

    _handlers: ClassVar[
        dict[CallsiteParameter, Callable[[str, inspect.Traceback], Any]]
    ] = {
        CallsiteParameter.PATHNAME: _pathname,
        CallsiteParameter.FILENAME: _filename,
        CallsiteParameter.MODULE: _module,
        CallsiteParameter.FUNC_NAME: _func_name,
        CallsiteParameter.LINENO: _lineno,
        CallsiteParameter.THREAD: _thread,
        CallsiteParameter.THREAD_NAME: _thread_name,
        CallsiteParameter.PROCESS: _process,
        CallsiteParameter.PROCESS_NAME: _process_name,
    }
    ```


Discussion:
@nathanneibauer I ran into the same issue, and made #603 based on your fix.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2f8df48..cf107bc7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,8 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
 - It is now possible to disable log level-padding in `structlog.dev.LogLevelColumnFormatter` and `structlog.dev.ConsoleRenderer`.
   [#599](https://github.com/hynek/structlog/pull/599)
 
+- The `structlog.processors.CallsiteParameterAdder` can now be pickled.
+  [#603](https://github.com/hynek/structlog/pull/603)
 
 
 ### Changed
diff --git a/src/structlog/processors.py b/src/structlog/processors.py
index e597eae4..ae873ce7 100644
--- a/src/structlog/processors.py
+++ b/src/structlog/processors.py
@@ -724,6 +724,46 @@ class CallsiteParameter(enum.Enum):
     PROCESS_NAME = "process_name"
 
 
+def _get_callsite_pathname(module: str, frame_info: inspect.Traceback) -> Any:
+    return frame_info.filename
+
+
+def _get_callsite_filename(module: str, frame_info: inspect.Traceback) -> Any:
+    return os.path.basename(frame_info.filename)
+
+
+def _get_callsite_module(module: str, frame_info: inspect.Traceback) -> Any:
+    return os.path.splitext(os.path.basename(frame_info.filename))[0]
+
+
+def _get_callsite_func_name(module: str, frame_info: inspect.Traceback) -> Any:
+    return frame_info.function
+
+
+def _get_callsite_lineno(module: str, frame_info: inspect.Traceback) -> Any:
+    return frame_info.lineno
+
+
+def _get_callsite_thread(module: str, frame_info: inspect.Traceback) -> Any:
+    return threading.get_ident()
+
+
+def _get_callsite_thread_name(
+    module: str, frame_info: inspect.Traceback
+) -> Any:
+    return threading.current_thread().name
+
+
+def _get_callsite_process(module: str, frame_info: inspect.Traceback) -> Any:
+    return os.getpid()
+
+
+def _get_callsite_process_name(
+    module: str, frame_info: inspect.Traceback
+) -> Any:
+    return get_processname()
+
+
 class CallsiteParameterAdder:
     """
     Adds parameters of the callsite that an event dictionary originated from to
@@ -767,33 +807,15 @@ class CallsiteParameterAdder:
     _handlers: ClassVar[
         dict[CallsiteParameter, Callable[[str, inspect.Traceback], Any]]
     ] = {
-        CallsiteParameter.PATHNAME: (
-            lambda module, frame_info: frame_info.filename
-        ),
-        CallsiteParameter.FILENAME: (
-            lambda module, frame_info: os.path.basename(frame_info.filename)
-        ),
-        CallsiteParameter.MODULE: (
-            lambda module, frame_info: os.path.splitext(
-                os.path.basename(frame_info.filename)
-            )[0]
-        ),
-        CallsiteParameter.FUNC_NAME: (
-            lambda module, frame_info: frame_info.function
-        ),
-        CallsiteParameter.LINENO: (
-            lambda module, frame_info: frame_info.lineno
-        ),
-        CallsiteParameter.THREAD: (
-            lambda module, frame_info: threading.get_ident()
-        ),
-        CallsiteParameter.THREAD_NAME: (
-            lambda module, frame_info: threading.current_thread().name
-        ),
-        CallsiteParameter.PROCESS: (lambda module, frame_info: os.getpid()),
-        CallsiteParameter.PROCESS_NAME: (
-            lambda module, frame_info: get_processname()
-        ),
+        CallsiteParameter.PATHNAME: _get_callsite_pathname,
+        CallsiteParameter.FILENAME: _get_callsite_filename,
+        CallsiteParameter.MODULE: _get_callsite_module,
+        CallsiteParameter.FUNC_NAME: _get_callsite_func_name,
+        CallsiteParameter.LINENO: _get_callsite_lineno,
+        CallsiteParameter.THREAD: _get_callsite_thread,
+        CallsiteParameter.THREAD_NAME: _get_callsite_thread_name,
+        CallsiteParameter.PROCESS: _get_callsite_process,
+        CallsiteParameter.PROCESS_NAME: _get_callsite_process_name,
     }
     _record_attribute_map: ClassVar[dict[CallsiteParameter, str]] = {
         CallsiteParameter.PATHNAME: "pathname",


Test patch:
diff --git a/tests/processors/test_processors.py b/tests/processors/test_processors.py
index 865c1f7d..cec06474 100644
--- a/tests/processors/test_processors.py
+++ b/tests/processors/test_processors.py
@@ -11,6 +11,7 @@
 import json
 import logging
 import os
+import pickle
 import sys
 import threading
 
@@ -492,6 +493,14 @@ def test_e2e(
 
         assert expected == actual
 
+    def test_pickeable_callsite_parameter_adder(self) -> None:
+        """
+        An instance of ``CallsiteParameterAdder`` can be pickled.  This
+        functionality may be used to propagate structlog configurations to
+        subprocesses.
+        """
+        pickle.dumps(CallsiteParameterAdder())
+
     @classmethod
     def make_processor(
         cls,