Darwinweb

Undoing Rails Monkey Patch to Logger

February 16, 2007     

New Ruby programmers coming from more static languages often overlook some of its most powerful features. One such feature is the ability to redefine existing methods at any time. This is referred to affectionately as monkey patching—probably because only a monkey would think this is a good idea. But just because monkey patching is bad in general doesn’t mean it doesn’t have its uses. The benefit is the unprecedented power of tweaking external code without modifying its source code files.

Rails uses this technique to make the Logger class skip its standard formatting procedure. This is done by replacing Logger’s format_message method. The benefit here is that the original logger.rb file is intact. Upgrades will go off seamlessly (unless the semantics of format_message are changed). The downside is any other code you’re using may no longer get the results it expects. In practice its a minor change unlikely to seriously affect many people.

But what if it does affect you? I faced this dilemma yesterday, and finding a solution turned out to be a great learning exercise about Ruby subtleties. When I started I loosely had the following concepts in mind:

Ruby modules act as namespaces, therefore I should be able to define a separate Logger class inside my module (henceforth referred to as Darwinweb).

The load method will re-include a file, restoring any methods that had been redefined.

The alias method will create another reference to a method which can be used if the old method is redefined.

The first thing I tried was to load("logger.rb") from within Darwinweb. I had hoped this would create the class Logger inside the Darwinweb namespace (ie. Darwinweb::Logger). But that’s not how load or require work. These methods always evaluate at the root namespace (Kernel).

To load a file into a specific context requires actually reading the code into a string and then using eval or module_eval. That just struck me as wrong.

The solution I eventually came to is satisfyingly simple:

module Darwinweb
  class Logger < ::Logger
    alias format_message old_format_message
  end
end

This created a new Logger class inheriting from the root-level Logger which has been patched by Rails. This new class is distinct because it resides in Darwinweb. It also will be automatically picked up by any other Logger references within Darwinweb while outsiders would have to reference @Darwinweb::Logger. A geeky sidenote is that the explicit root reference to ::Logger isn’t strictly necessary for the first internal definition of Logger since it will find the root Logger before an internal Logger is defined.

When Rails redefines format_message, it aliases the original. This is standard operating procedure when monkey-patching, and it pays off. We simply alias the old method back and everything works as intended both within and outside of Rails.

If the exact mechanics of this are a little confusing, then I highly recommend David Black’s Ruby for Rails for an in-depth discussion of core ruby semantics.

albert says…
March 6, 2007 at 3:01AM

this also works


>> module Foo
>> end
=> nil
>> Foo.class_eval do
?> def hello
>> puts “hello”
>> end
>> end
>> Foo.instance_methods
=> [“hello”]

albert says…
March 6, 2007 at 3:02AM

You would, of course have to define a class instead :)