"What is a functional approach to logging and transactions" is a question I get asked sometimes. So here's my take.
First, there is never a perfect answer! It depends on your tolerance for impurity, what's compatible with the rest of the code and what other maintainers expect.
I use F#, which generally has a pragmatic approach to this kind of thing.
In Haskell or purist FP Scala (e.g. with Cats/Scalaz) the standard approach would be to use a writer monad, but for F#/OCaml/ReasonML etc, that would probably be overkill.
So, for logging I normally just have a global ILogger instance that I talk to. It's a lot easier than (a) passing in a logger to every function, or (b) using a writer monad in an impure language.
For unit tests, I either set the logger to a dummy version, or I initialize it to use the same logger that I use in production (e.g. Serilog).
For transactions, my approach depends.
If you're using a pipeline approach where all I/O is at the edges (as explained in my talk), then I do transactions as part of the I/O so my core code doesn't see it. If anything fails, a rollback occurs and nothing is saved.
If you have a more complicated design with lots of IO interleaved with business logic, the Interpreter/FreeMonad approach can be useful. (my version for turtles). Again, you would start a transaction when you start interpreting the data structure, and if anything fails, a rollback occurs and the whole transaction is aborted.
If you are using a transaction to make sure that two different services have been written to (e.g a database and a message queue), then I would avoid a two-phase commit ("Starbucks Does Not Use Two-Phase Commit") or Distributed Transaction Coordinator and instead use the so called "Transactional Outbox" pattern.
And if the domain is even more complicated, then you might even need a special broker to do the coordination. The "Enterprise Integration Patterns" book has some good stuff for this.
Finally, you can take a leaf from real-world transactions that are distributed over time and space (e.g in banking and accounting). In these systems, there are no long-duration transactions. Instead, there is a separate process that checks that everything matches up ("reconciliation") and if anything goes wrong a "compensating transaction" occurs (such as reversing a credit when a check bounces!)
Udi Dahan did a talk at DDDEU on this topic which has some useful ideas.