At the last project I worked on, we did a lot of work to optimize our logging, as it was being done on low spec TV set-top-boxes, and we were burning a ton of CPU cycles just doing formatting (lots of JSON serialization). What many devs fail to realise is that, unless extra effort is made (as in the Erlang system in the OP), every log operation you call is a synchronous, blocking call that can actually be relatively expensive due to all the serialization and I/O that goes on.
Async logging can help of course (necessary if you want to send logs over the network). But you can still spend a lot of CPU just formatting the log lines themselves.
The solution we decided on was similar to what you'd do in a C/C++ project: for release/production, add a build step that removes all logging commands below the logging level for the target. For example in C, you'd usually use macros for this, and if your macro wasn't defined in release builds, none of the logging code would be included in your executable.
This is great for performance but does have an operational trade off. Ops people might want to be able to change the logging level of a system to diagnose live issues. But if I think about all the CPU cycles and HDD storage spent on logging (and often those wasted cycles have a very real impact on UX or server latency), then I think in many cases it is worth stripping out anything lower than Error level as standard.