Undoing Rails Monkey Patch to Logger
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
albert says…
March 6, 2007 at 3:02AM
You would, of course have to define a class instead :)