Logging tricks: duplicate and broadcast

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:

ActiveAdmin background process log 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.

Useful links

Comment

Has something on your mind? Tweet it!
Photo by Thomas Peham