diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a12c8f..7a0e35c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
 
 - `structlog.threadlocal.tmp_bind()` now also works with `BoundLoggerLazyProxy` (in other words: before anything is bound to a bound logger).
 
+- stdlib: `ProcessorFormatter` can now be told to not render the log record message using `getMessage` and just `str(record.msg)` instead.
+  [#550](https://github.com/hynek/structlog/issues/550)
+
 - stdlib: `structlog.stdlib.BoundLogger.exception()`'s handling of`LogRecord.exc_info` is now set consistent with `logging`.
   [#571](https://github.com/hynek/structlog/issues/571)
   [#572](https://github.com/hynek/structlog/issues/572)
diff --git a/src/structlog/stdlib.py b/src/structlog/stdlib.py
index 9e82035..5e05e9b 100644
--- a/src/structlog/stdlib.py
+++ b/src/structlog/stdlib.py
@@ -964,6 +964,10 @@ class ProcessorFormatter(logging.Formatter):
             This parameter exists for historic reasons. Please use *processors*
             instead.
 
+        use_get_message:
+            If True, use ``record.getMessage`` to get a fully rendered log
+            message, otherwise use ``str(record.msg)``. (default: True)
+
     Raises:
 
         TypeError: If both or neither *processor* and *processors* are passed.
@@ -976,6 +980,7 @@ class ProcessorFormatter(logging.Formatter):
     .. deprecated:: 21.3.0
        *processor* (singular) in favor of *processors* (plural). Removal is not
        planned.
+    .. versionadded:: 23.3.0 *use_get_message*
     """
 
     def __init__(
@@ -987,6 +992,7 @@ class ProcessorFormatter(logging.Formatter):
         keep_stack_info: bool = False,
         logger: logging.Logger | None = None,
         pass_foreign_args: bool = False,
+        use_get_message: bool = True,
         *args: Any,
         **kwargs: Any,
     ) -> None:
@@ -1011,6 +1017,7 @@ class ProcessorFormatter(logging.Formatter):
         self.keep_stack_info = keep_stack_info
         self.logger = logger
         self.pass_foreign_args = pass_foreign_args
+        self.use_get_message = use_get_message
 
     def format(self, record: logging.LogRecord) -> str:
         """
@@ -1043,7 +1050,9 @@ class ProcessorFormatter(logging.Formatter):
             logger = self.logger
             meth_name = record.levelname.lower()
             ed = {
-                "event": record.getMessage(),
+                "event": record.getMessage()
+                if self.use_get_message
+                else str(record.msg),
                 "_record": record,
                 "_from_structlog": False,
             }

diff --git a/tests/test_stdlib.py b/tests/test_stdlib.py
index 626b1a7..569bbbd 100644
--- a/tests/test_stdlib.py
+++ b/tests/test_stdlib.py
@@ -1189,6 +1189,34 @@ class TestProcessorFormatter:
 
         assert not records
 
+    def test_use_get_message_false(self):
+        """
+        If use_get_message_is False, the event is obtained using
+        str(record.msg) instead of calling record.getMessage. That means
+        positional formatting is not performed.
+        """
+        event_dicts = []
+
+        def capture(_, __, ed):
+            event_dicts.append(ed.copy())
+
+            return str(ed)
+
+        proc = ProcessorFormatter(processors=[capture], use_get_message=False)
+
+        record = logging.LogRecord(
+            "foo",
+            logging.INFO,
+            "path.py",
+            42,
+            "le msg: %s",
+            ("keep separate",),
+            None,
+        )
+
+        assert proc.format(record)
+        assert "le msg: %s" == event_dicts[0]["event"]
+
 
 @pytest_asyncio.fixture(name="abl")
 async def _abl(cl):

ProcessorFormatter should be able to pass the message unformatted
https://github.com/hynek/structlog/blob/f7145a6a815083a748ec2e41a1d66dd63ce6c411/src/structlog/stdlib.py#L1016

The problematic pipeline is logging ->  ProcessorFormatter -> structlog -> Sentry.

Currently ProcessorFormatter calls `getMessage` on the log record.

This means that the formatted log message from `logging` becomes the "event" field in `structlog`. This causes duplicates if structlog is integrated with Sentry, which groups messages by the "event" field. If the "event" field wasn't formatted, the messages would get grouped correctly.


There should be a flag on ProcessorFormatter that allows it to pass the log message unformatted, like `"event": str(record.msg),` ([`LogRecord.msg` can be anything supporting `__str__`](https://docs.python.org/3/library/logging.html#logging.LogRecord)).

Passing positional args is already supported thanks to the `pass_foreign_args` flag.
tests/test_stdlib.py::TestProcessorFormatter::test_use_get_message_false
tests/test_stdlib.py::TestLoggerFactory::test_deduces_correct_name tests/test_stdlib.py::TestLoggerFactory::test_ignores_frames tests/test_stdlib.py::TestLoggerFactory::test_deduces_correct_caller tests/test_stdlib.py::TestLoggerFactory::test_stack_info tests/test_stdlib.py::TestLoggerFactory::test_no_stack_info_by_default tests/test_stdlib.py::TestLoggerFactory::test_find_caller tests/test_stdlib.py::TestLoggerFactory::test_sets_correct_logger tests/test_stdlib.py::TestLoggerFactory::test_positional_argument_avoids_guessing tests/test_stdlib.py::TestFilterByLevel::test_filters_lower_levels tests/test_stdlib.py::TestFilterByLevel::test_passes_higher_levels tests/test_stdlib.py::TestBoundLogger::test_proxies_to_correct_method[debug] tests/test_stdlib.py::TestBoundLogger::test_proxies_to_correct_method[info] tests/test_stdlib.py::TestBoundLogger::test_proxies_to_correct_method[warning] tests/test_stdlib.py::TestBoundLogger::test_proxies_to_correct_method[error] tests/test_stdlib.py::TestBoundLogger::test_proxies_to_correct_method[exception] tests/test_stdlib.py::TestBoundLogger::test_proxies_to_correct_method[critical] tests/test_stdlib.py::TestBoundLogger::test_proxies_log tests/test_stdlib.py::TestBoundLogger::test_positional_args_proxied tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_attributes[name] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_attributes[level] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_attributes[parent] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_attributes[propagate] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_attributes[handlers] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_attributes[disabled] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[addHandler-method_args0] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[removeHandler-method_args1] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[hasHandlers-None] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[callHandlers-method_args3] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[handle-method_args4] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[setLevel-method_args5] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[getEffectiveLevel-None] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[isEnabledFor-method_args7] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[findCaller-None] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[makeRecord-method_args9] tests/test_stdlib.py::TestBoundLogger::test_stdlib_passthrough_methods[getChild-method_args10] tests/test_stdlib.py::TestBoundLogger::test_exception_exc_info tests/test_stdlib.py::TestBoundLogger::test_exception_exc_info_override tests/test_stdlib.py::TestBoundLogger::test_proxies_bind tests/test_stdlib.py::TestBoundLogger::test_proxies_new tests/test_stdlib.py::TestBoundLogger::test_proxies_unbind tests/test_stdlib.py::TestBoundLogger::test_proxies_try_unbind tests/test_stdlib.py::TestBoundLogger::test_async_log_methods[debug] tests/test_stdlib.py::TestBoundLogger::test_async_log_methods[info] tests/test_stdlib.py::TestBoundLogger::test_async_log_methods[warning] tests/test_stdlib.py::TestBoundLogger::test_async_log_methods[error] tests/test_stdlib.py::TestBoundLogger::test_async_log_methods[critical] tests/test_stdlib.py::TestBoundLogger::test_alog tests/test_stdlib.py::TestBoundLogger::test_aexception_exc_info_true tests/test_stdlib.py::TestBoundLogger::test_aexception_exc_info_explicit tests/test_stdlib.py::TestPositionalArgumentsFormatter::test_formats_tuple tests/test_stdlib.py::TestPositionalArgumentsFormatter::test_formats_dict tests/test_stdlib.py::TestPositionalArgumentsFormatter::test_positional_args_retained tests/test_stdlib.py::TestPositionalArgumentsFormatter::test_nop_no_args tests/test_stdlib.py::TestPositionalArgumentsFormatter::test_args_removed_if_empty tests/test_stdlib.py::TestAddLogLevelNumber::test_log_level_number_added[critical-50] tests/test_stdlib.py::TestAddLogLevelNumber::test_log_level_number_added[exception-40] tests/test_stdlib.py::TestAddLogLevelNumber::test_log_level_number_added[error-40] tests/test_stdlib.py::TestAddLogLevelNumber::test_log_level_number_added[warn-30] tests/test_stdlib.py::TestAddLogLevelNumber::test_log_level_number_added[warning-30] tests/test_stdlib.py::TestAddLogLevelNumber::test_log_level_number_added[info-20] tests/test_stdlib.py::TestAddLogLevelNumber::test_log_level_number_added[debug-10] tests/test_stdlib.py::TestAddLogLevelNumber::test_log_level_number_added[notset-0] tests/test_stdlib.py::TestAddLogLevel::test_log_level_added tests/test_stdlib.py::TestAddLogLevel::test_log_level_alias_normalized tests/test_stdlib.py::TestAddLoggerName::test_logger_name_added tests/test_stdlib.py::TestAddLoggerName::test_logger_name_added_with_record tests/test_stdlib.py::TestExtraAdder::test_add_extra[None-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra[allow1-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra[allow2-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra[allow3-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra[allow4-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra[allow5-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra[allow6-misses6] tests/test_stdlib.py::TestExtraAdder::test_add_extra[allow7-misses7] tests/test_stdlib.py::TestExtraAdder::test_add_extra[allow8-None] tests/test_stdlib.py::TestExtraAdder::test_no_record tests/test_stdlib.py::TestExtraAdder::test_add_extra_e2e[None-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra_e2e[allow1-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra_e2e[allow2-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra_e2e[allow3-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra_e2e[allow4-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra_e2e[allow5-None] tests/test_stdlib.py::TestExtraAdder::test_add_extra_e2e[allow6-misses6] tests/test_stdlib.py::TestExtraAdder::test_add_extra_e2e[allow7-misses7] tests/test_stdlib.py::TestExtraAdder::test_add_extra_e2e[allow8-None] tests/test_stdlib.py::TestRenderToLogKW::test_default tests/test_stdlib.py::TestRenderToLogKW::test_add_extra_event_dict tests/test_stdlib.py::TestRenderToLogKW::test_handles_special_kw tests/test_stdlib.py::TestProcessorFormatter::test_foreign_delegate tests/test_stdlib.py::TestProcessorFormatter::test_clears_args tests/test_stdlib.py::TestProcessorFormatter::test_pass_foreign_args_true_sets_positional_args_key tests/test_stdlib.py::TestProcessorFormatter::test_log_dict tests/test_stdlib.py::TestProcessorFormatter::test_foreign_pre_chain tests/test_stdlib.py::TestProcessorFormatter::test_foreign_pre_chain_add_logger_name tests/test_stdlib.py::TestProcessorFormatter::test_foreign_chain_can_pass_dictionaries_without_excepting tests/test_stdlib.py::TestProcessorFormatter::test_foreign_pre_chain_gets_exc_info tests/test_stdlib.py::TestProcessorFormatter::test_foreign_pre_chain_sys_exc_info tests/test_stdlib.py::TestProcessorFormatter::test_other_handlers_get_original_record tests/test_stdlib.py::TestProcessorFormatter::test_formatter_unsets_exc_info[True] tests/test_stdlib.py::TestProcessorFormatter::test_formatter_unsets_exc_info[False] tests/test_stdlib.py::TestProcessorFormatter::test_formatter_unsets_stack_info[True] tests/test_stdlib.py::TestProcessorFormatter::test_formatter_unsets_stack_info[False] tests/test_stdlib.py::TestProcessorFormatter::test_native tests/test_stdlib.py::TestProcessorFormatter::test_native_logger tests/test_stdlib.py::TestProcessorFormatter::test_foreign_pre_chain_filter_by_level tests/test_stdlib.py::TestProcessorFormatter::test_processor_and_processors tests/test_stdlib.py::TestProcessorFormatter::test_no_renderer tests/test_stdlib.py::TestProcessorFormatter::test_remove_processors_meta tests/test_stdlib.py::TestProcessorFormatter::test_non_string_message_warning tests/test_stdlib.py::TestProcessorFormatter::test_logrecord_exc_info tests/test_stdlib.py::TestAsyncBoundLogger::test_sync_bl tests/test_stdlib.py::TestAsyncBoundLogger::test_protocol tests/test_stdlib.py::TestAsyncBoundLogger::test_correct_levels[critical] tests/test_stdlib.py::TestAsyncBoundLogger::test_correct_levels[exception] tests/test_stdlib.py::TestAsyncBoundLogger::test_correct_levels[error] tests/test_stdlib.py::TestAsyncBoundLogger::test_correct_levels[warn] tests/test_stdlib.py::TestAsyncBoundLogger::test_correct_levels[warning] tests/test_stdlib.py::TestAsyncBoundLogger::test_correct_levels[info] tests/test_stdlib.py::TestAsyncBoundLogger::test_correct_levels[debug] tests/test_stdlib.py::TestAsyncBoundLogger::test_log_method tests/test_stdlib.py::TestAsyncBoundLogger::test_exception tests/test_stdlib.py::TestAsyncBoundLogger::test_exception_do_not_overwrite tests/test_stdlib.py::TestAsyncBoundLogger::test_bind_unbind tests/test_stdlib.py::TestAsyncBoundLogger::test_integration tests/test_stdlib.py::test_recreate_defaults[None] tests/test_stdlib.py::test_recreate_defaults[45]