I did a similar thing, in C++, 3 decades ago. I used a macro, FUNC(), that I would put at the start of functions. It took no arguments and declared a local instance, using the __FUNC__ preprocessor builtin to pass the function name to the Trace constructor:
Trace trace##__LINE__(__FUNC__);
The Trace instance would generate one log on construction and another on destruction. It also kept track of function call nesting (a counter) in a static member that would increment in the constructor and decrement in the destructor. It was inherently single-threaded, because I used a static member, but it could be adapted to multiple threads using thread local storage. I paired it with a LINE("Var x is " << x); macro for arbitrary ostreams-style logging. And building on that, EXPR(x) would do LINE(#x " = " << (x)). The output was along the lines of:
,- A::f()
| ,- A::g()
| | ,- B::B()
| | `- B::~B()
| | x = 12
| | About to do a thing...
| | ,- A::doAThing()
| | `- A::doAThing()
| `- A::g()
`- A::f()
The macros could be disabled (defined to do nothing) by a preprocessor symbol.
gcc's -finstrument-functions can also add the function call traces without changing the code I think