Everybody loves logs! And the more of them we have, the better. They are so useful! Today I’m going to share a bit non-trivial approach to logging some background process: broadcasting messages into WebSocket channel and combining several loggers to work as a single one seamlessly.
Broadcast your progress
Imagine some long-running background process that you’d like to monitor. Let’s assume you already have a nice web dashboard, and you want to display the detailed progress information of what’s happening right now. One of the possible solutions here would be to use WebSocket channel to receive the background job’s progress information and messages in real-time.
Nice! All we need is to broadcast a message into a proper
ActionCable channel each time you have something to show. But wait… 🤔 What if the background processing involves dozens of classes and each of them has something interesting to send? Should we add a broadcasting code to each of them? It does not sound cool anymore, right?
Luckily, I have a solution: use the Logger. It provides a common way to gather process details, has an easy-to-use interface, and can easily be changed in the future to use a different type of transport or storage.
Creating your own logger implementation is quite simple. You only need to overload a
#write method implementation of a well-known Ruby
class ChannelLogWriter < Logger def initialize(channel, identifier) @channel = channel @identifier = identifier end def write(message) @channel.broadcast_to(@identifier, text: message) end end
This simple logger implementation takes a channel class and an identifier (an object you are broadcasting data for) and then, when the
#write method is called, send the given message by using the channel’s
That’s it! Now you can create Rails style logger and use it to send messages into a WebSocket:
logger = ActiveSupport::Logger.new(ChannelLogWriter.new(MyChannel, a_user)) logger.info 'Some text message'
and then catch them on your dashboard:
Everything works fine. Your complex class structure sends all the needed data into WebSocket by using a trivial logger interface and you are ready to celebrate. Suddenly, an irritating thought comes into your mind: all the data you’ve sent into a WebSocket disappears without a trace. You need to save it somewhere for future use. Oh, no! Now you have to add something like
Rails.logger.info everywhere again! 😢
No worries! There is a better solution to it in the depths of Rails. You can make
ActiveSupport::Logger class instance not only process a message by itself but also transmit it to another logger! Here’s how to do it:
channel_logger = ActiveSupport::Logger.new(ChannelLogWriter.new(MyChannel, a_user)) file_logger = ActiveSupport::Logger.new(some_log_file) dual_logger = file_logger.extend(ActiveSupport::Logger.broadcast(channel_logger))
Now, when you send a message to the
dual_logger it will be both saved into a log file and sent to a WebSocket.