I use a function STR that returns a string and can take up to twenty intrinsic scalars. Then the string can be written to a file(s) or passed to another procedure in some way or another. If a float needs refined I can pass it as a call to FMT to specify a specific format. I can then pass the string to a number of other procedures to record it in a log, write it to multiple files, stick it in an SQLite3 file, pass it over the network, …
The M_framework module is more recent and includes a logging utility but the simple components are in M_msg. If you use fpm(1) they are trivial to try. If all your messages use just a single format a print procedure is not bad; but if writing a lot of unique messages using something like string allows you to see the full message where it is being called which can make the code clearer in my experience.