Skip to content

Add option to truncate boring stack frames #74

Open

Description

@tepedersen

Thanks for an excellent lib!

In a project with fastapi I found myself looking at insanely long stacktraces which aren't just boring, but also irrelevant to my debugging of errors in production. My suggestion is this, that format takes another kwarg "truncate_suppressed_to_lines" (or something similar). If this is set to 2, it would truncate for example 10 "boring" frames down to 1 boring frame before and one after the truncated part of the stack:
File: ....
[ --- 8 frames truncated ---]
File: ...

Given truncate_suppressed_to_lines=4 you would get 2 boring frames, then 6 frames truncated, then 2 boring frames, etc. Or perhaps it is more useful with 3 boring, then truncated, then 1?

Looking at the source code this looks fairly easy to do, it only looks like some changes in format_stack() is needed to achieve it.

If this is interesting I can send you a PR with this, also I can include typing through a .pyi for the public facing methods in init.py if you'd like?

Activity

  1. cknd commented on Jan 19, 2026

    @cknd
    Owner

    Hi, thank you!

    If possible, can you post an example of such a long traceback so I can better understand the issue? The current option to write just 1 line per boring frame is pretty compact already compared to standard python tracebacks, so I'd like to learn more

    I also thought for a moment about how to fit it into the existing options without making it too confusing. How about a boolean that simply controls whether or not the frames below the first within boring code are rendered? That should solve the too many 'irrelevant lines' problem while being relatively simple to understand. (Adding configurable depth after which frames are skipped seems like one step too far in terms of config complexity. The current method that shows the first frame in boring code incl arguments passed is the right balance, it basically says "consider everything in these paths a black box, but do show me how my code called the black box")

        suppressed_paths: list of regex patterns
            Set less verbose formatting for frames whose code lives in certain paths
            (e.g. library code). Files whose path matches any of the given regex
            patterns will be considered boring. The first call to boring code is
            rendered with fewer code lines (but with argument values still visible),
    -       while deeper calls within boring code get a single line and no variable
    -       values.
    +       while deeper calls within boring code are rendered as a single line
    +       (or skipped completely if `skip_suppressed_paths` is True).
    
            Example: To hide numpy internals from the traceback, set
            `suppressed_paths=[r"lib/python.*/site-packages/numpy"]`
            or
            `suppressed_paths=[re.compile(r"lib/python.*/site-packages/numpy")]`
    +
    +  skip_suppressed_paths: bool (default False)
    +     completely omit frames beyond the first call into boring code
    +     (= files matched by `suppressed_paths`)

    Some open questions still -- I guess we'd still need to count how many frames will be skipped, and only if it's more than one or so display something like "Skipped due to regex match", because skipping only makes sense if the frames we're skipping would've taken considerably more space than the current behavior of just printing 1 line per frame.. but it can be an opinionated number, doesn't need to be configurable IMO.

    Overall, not completely sure about the tradeoff re complexity, hence my interest in an example

  2. cknd commented on Jan 19, 2026

    @cknd
    Owner

    ah btw - what if we simply skip the newlines between the 1-line prints for boring frames? bam, 50% reduction of spam. should be easy to change

  3. tepedersen commented on Jan 21, 2026

    @tepedersen
    Author

    Hey! Here's a sample of what is a very simple stack trace from a basic test of a global exception handler i FastAPI. It is an ultrasimple endpoint directly attached to the main app instance of FastAPI:

    def test_global_exception_handler_multipart_urlencode(client: TestClient, caplog: pytest.LogCaptureFixture):
    
        # Defining a temporary route that raises an error
        @app.post("/force-error/{path_param}")
        def force_error_post(path_param: str) -> None:
            raise ValueError("Dette er en simulert uventet feil")
    
        with caplog.at_level("ERROR"):
            response_post = client.post("/force-error/testvalue", data={"key": "value", "password": "rosebud"})
    
        assert response_post.status_code == 500
        data_post = response_post.json()
        app_err = AppErrorDetail.model_validate(data_post["detail"])
        print(data_post)
        assert app_err.i18nkey == AppErrorStringKey.INTERNAL_SERVER_ERROR
        assert "Dette er en simulert uventet feil" not in response_post.text
        assert "POST" in caplog.text
        assert "Dette er en simulert uventet feil" in caplog.text
        assert "path_params:({'path_param': 'testvalue'})" in caplog.text
        assert "body=(key=['value'], password=['*******'])" in caplog.text
        Path("/tmp/test_app_exception.log").write_text(caplog.text)
    

    Se the attached log file... it's insanely long, with 21 lines of internals of fastapi and starlette (which fastapi builds on) alone, which are pretty much useless for debugging the errors in my own code. One such log entry i 6.8k...

    test_app_exception.log

  4. cknd commented on Jan 23, 2026

    @cknd
    Owner

    I see! thanks

    I've playing around with these outputs a little bit to try various ideas.

    original.txt
    log_skipped.txt
    log_singleline.txt

    Somehow this mockup of doing skipped frames feels even more cluttered / confusing than the original The single-line version actually creates a block of text that I can easily visually skip over, and doesn't throw away any information (you can theoretically reconstruct all information of the default stracktrace with the file & lineno given). Could this be a good compromise?

  5. cknd commented on Jan 23, 2026

    @cknd
    Owner

    maybe just a question of formatting though - this variant of skipping is a little better

    log_skipped_2.txt

  6. cknd commented on Jan 23, 2026

    @cknd
    Owner

    alright, conclusion: I'm open to both, the singleline version, or skipping with well chosen formatting (perhaps the key is not to indent the "skipped x frames" message)

    and definitely happy to take a PR (though I can't promise a super quick review turnaround if the diff is complex)

  7. cknd commented on Jan 23, 2026

    @cknd
    Owner

    your [ --- 8 frames truncated ---] is perfect actually

    log_skipped_3.txt

  8. cknd commented on Jan 23, 2026

    @cknd
    Owner

    side note: I had trouble initially to understand the intended usage of truncate_suppressed_to_lines -- maybe a simple bool (e.g. "skip_suppressed_frames - show only the first and last of a consecutive block of boring frames") is enough and reduces the cognitive hurdle

  9. tepedersen commented on Jan 23, 2026

    @tepedersen
    Author

    A simple bool and including just the first and last of the block of boring frames sounds like the best solution, agreed! I have some vacation coming up so should have time to make a PR with the suggested changes.

    Want me to include a .pyi with typing for the public facing api as well?

  10. cknd commented on Jan 24, 2026

    @cknd
    Owner

    awesome thanks!

    the pyi sounds like something for a separate PR since the project doesn't have it yet for anything

  11. tepedersen commented on Jan 30, 2026

    @tepedersen
    Author

    Implemented the proposed feature and sent you a PR. I'll look into creating the typing info tomorrow.

  12. tepedersen commented on Mar 25, 2026

    @tepedersen
    Author

    Hey, had the time to look at the PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions