But the real problem, and top-level issue, is that you are constantly rooting around the very lowest level of your code, and it is difficult and labor-intensive to get a higher level view of what's going on.
On the other hand, well-designed logging code is there when you need it, can provide a view at any level of abstraction that you'd like (if you've written the logging code), and allows you to go forward and backward in time easily and repeatedly.
The project that really pushed me in this direction was working on a cluster. My piece of the system would start on one of the nodes, and delegate work to a fixed number of other nodes, or to all nodes. Out of necessity, I put a lot of work into debugging code, taking care to include the right information in each line (node id, time to nsec, source id, line number), carefully formatted to enable sorting and filtering, log rotation to make sure that I could focus on recent events or a longer timescale, and a lot of attention to the information included in each line and how it was formatted.
Because this was a long-running distributed system, I would often need to gather logs from across the cluster, going back hours or days, and merge and sort the files, and then debugging could start.
Going along with logging was heavy use of assertions, which were enabled even in production. I definitely did not want my code going forward doing random unanticipated crazy things past the point of an assertion failure.