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 Logger
class.
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 #broadcast_to
method.
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:
Duplicating logging
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.
Comment
Has something on your mind? Tweet it!