diff --git a/docs/reference/installation.md b/docs/reference/installation.md index f3c8da8..23d7950 100644 --- a/docs/reference/installation.md +++ b/docs/reference/installation.md @@ -85,7 +85,7 @@ formatter = StdlibFormatter( #### Limiting stack traces [_limiting_stack_traces] -The `StdlibLogger` automatically gathers `exc_info` into ECS `error.*` fields. If you’d like to control the number of stack frames that are included in `error.stack_trace` you can use the `stack_trace_limit` parameter (by default all frames are collected): +The `StdlibLogger` automatically gathers `exc_info` into ECS `error.*` fields. If you’d like to control the number of stack frames that are included in `error.stack_trace` you can use the `stack_trace_limit` parameter (by default all frames are collected). Positive values include frames starting from the caller's frame. Negative values include the last `N` frames (closest to the error): ```python from ecs_logging import StdlibFormatter @@ -94,6 +94,10 @@ formatter = StdlibFormatter( # Only collects 3 stack frames stack_trace_limit=3, ) +formatter = StdlibFormatter( + # Collects the last 2 frames (closest to the error) + stack_trace_limit=-2, +) formatter = StdlibFormatter( # Disable stack trace collection stack_trace_limit=0, @@ -322,4 +326,3 @@ labels: ::::::: For more information, see the [Filebeat reference](beats://reference/filebeat/configuring-howto-filebeat.md). - diff --git a/ecs_logging/_stdlib.py b/ecs_logging/_stdlib.py index 8e541d7..79843f6 100644 --- a/ecs_logging/_stdlib.py +++ b/ecs_logging/_stdlib.py @@ -91,7 +91,9 @@ def __init__( :param int stack_trace_limit: Specifies the maximum number of frames to include for stack traces. Defaults to ``None`` which includes all available frames. - Setting this to zero will suppress stack traces. + Setting this to zero will suppress stack traces. Positive values + include frames starting from the caller's frame. Negative values + include the last ``N`` frames (closest to the error). This setting doesn't affect ``LogRecord.stack_info`` because this attribute is typically already pre-formatted. :param Optional[Dict[str, Any]] extra: @@ -116,13 +118,7 @@ def __init__( if stack_trace_limit is not None: if not isinstance(stack_trace_limit, int): - raise TypeError( - "'stack_trace_limit' must be None, or a non-negative integer" - ) - elif stack_trace_limit < 0: - raise ValueError( - "'stack_trace_limit' must be None, or a non-negative integer" - ) + raise TypeError("'stack_trace_limit' must be None or an integer") if ( not isinstance(exclude_fields, collections.abc.Sequence) @@ -283,7 +279,7 @@ def _record_error_stack_trace(self, record: logging.LogRecord) -> Optional[str]: if ( record.exc_info and record.exc_info[2] is not None - and (self._stack_trace_limit is None or self._stack_trace_limit > 0) + and (self._stack_trace_limit is None or self._stack_trace_limit != 0) ): return ( "".join(format_tb(record.exc_info[2], limit=self._stack_trace_limit)) diff --git a/tests/test_stdlib_formatter.py b/tests/test_stdlib_formatter.py index d993186..917daa8 100644 --- a/tests/test_stdlib_formatter.py +++ b/tests/test_stdlib_formatter.py @@ -208,7 +208,16 @@ def test_exc_info_false_does_not_raise(logger): assert "error" not in ecs -def test_stack_trace_limit_traceback(logger): +@pytest.mark.parametrize( + ("stack_trace_limit", "expected_in", "expected_not_in"), + [ + (2, ("f()", "g()"), ("h()",)), + (-2, ("h()",), ("f()", "g()")), + ], +) +def test_stack_trace_limit_traceback( + stack_trace_limit, expected_in, expected_not_in, logger +): def f(): g() @@ -220,7 +229,9 @@ def h(): stream = StringIO() handler = logging.StreamHandler(stream) - handler.setFormatter(ecs_logging.StdlibFormatter(stack_trace_limit=2)) + handler.setFormatter( + ecs_logging.StdlibFormatter(stack_trace_limit=stack_trace_limit) + ) logger.addHandler(handler) logger.setLevel(logging.DEBUG) @@ -231,8 +242,8 @@ def h(): ecs = json.loads(stream.getvalue().rstrip()) error_stack_trace = ecs["error"].pop("stack_trace") - assert all(x in error_stack_trace for x in ("f()", "g()")) - assert "h()" not in error_stack_trace + assert all(x in error_stack_trace for x in expected_in) + assert all(x not in error_stack_trace for x in expected_not_in) assert ecs["error"] == { "message": "error!", "type": "ValueError", @@ -245,11 +256,7 @@ def h(): def test_stack_trace_limit_types_and_values(): with pytest.raises(TypeError) as e: ecs_logging.StdlibFormatter(stack_trace_limit="a") - assert str(e.value) == "'stack_trace_limit' must be None, or a non-negative integer" - - with pytest.raises(ValueError) as e: - ecs_logging.StdlibFormatter(stack_trace_limit=-1) - assert str(e.value) == "'stack_trace_limit' must be None, or a non-negative integer" + assert str(e.value) == "'stack_trace_limit' must be None or an integer" @pytest.mark.parametrize(