So something that tends to be rather
global like a log or a configuration,
you would suggest to put in the IO
monad? From looking at (an admittedly
very limited set of) examples, I come
to think that Haskell code tends to be
either pure (i.e., not monadic at all)
or in the IO monad. Or is this a
misconception?
I think this is a misconception, only the IO monad is not pure. monads like Write/T/Reader/T/State/T/ST monads are purely functional still. You can write a pure function which uses any of these monads internally like this completely useless example:
foo :: Int -> Int
foo seed = flip execState seed $ do
modify $ (+) 3
modify $ (+) 4
modify $ (-) 2
All this is doing is threading/plumbing the state implicitly, what you would do yourself by hand explicitly, the do-notation here just gives you some nice syntactic sugar to make it look imperative. You can't do any IO actions here, you can't call any foreign functions. ST monad lets you have real mutable references in a local scope while having a pure function interface and, you can't do any IO actions in there it's purely functional still.
You can't avoid some IO actions but you don't want to fallback to IO for everything because that is where anything can go, missiles can be lunched, you got no control. Haskell has abstractions to control effectful computations at varying degrees of safety/purity, IO monad should be the last resort (but you can't avoid it completely).
In your example I think you should stick to using monad transformers or a custom made monad that does the same as composing them with transformers. I've never written a custom monad (yet) but i've used monad tranformers quite a bit (my own code not at work), don't worry about them so much use them and it's not as bad as you think.
Have you seen the chapter from real world haskell that uses monad transformers? here.