How to spy on a Hash in Ruby

February 24, 2010

Let's say you're dealing with a large Rails codebase and you've got a Hash stored in a global variable or a constant and you want to know who is changing that Hash. Here's a contrived example:

IMPORTANT_STUFF = {
  :password => "too many secrets"
}

def change_password(h)
  h[:password] = "FAIL"
end

def print_password
  puts IMPORTANT_STUFF[:password]
end

print_password
change_password(IMPORTANT_STUFF)
print_password

Here it's pretty obvious where the Hash gets changed, but as I said, imagine you are trying to figure this out in a much larger codebase. Something is changing the value of IMPORTANT_STUFF and you don't know what. So how do you figure out what is? Easy, you do what Lester Freeman would do!

Lester Freeman from The Wire

We set up a sting! We put a wire tap on IMPORTANT_STUFF and monitor all communication with IMPORTANT_STUFF. So how do we do that? Let's create a class that proxies all communication with a Hash:

class HashSpy

  def initialize(hash={})
    @hash = hash
  end

  def method_missing(method_name, *args, &block)
    puts "***** hash access"
    puts "  before: #{@hash.inspect}"
    r = @hash.send(method_name, *args, &block)
    puts "  after: #{@hash.inspect}"
    puts "  backtrace:\n    #{caller.join("\n    ")}"
    r
  end

end

This uses a couple of interesting Ruby techniques. First, we just pass the actual Hash to the constructor. Then, we use method missing so that any method that is called on the HashSpy will be then called on the Hash and the return value of that method call with be called instead. Note that in Ruby 1.8, this isn't a transparent proxy because if you called class on the HashSpy, you would get HashSpy, not Hash. In Ruby 1.9, you can have your object inherit from BasicObject, which won't have those methods, making it easier to be a transparent proxy. In Ruby 1.8, you can use Jim Weirich's Blank Slate pattern

In HashSpy's method missing, we use caller to get a backtrace of the current call stack, which will tell us who the perpetrator is.

So, if we just change IMPORTANT_STUFF to be created like this:

IMPORTANT_STUFF = HashSpy.new(
  :password => "too many secrets"
)

Now when we run the program, we'll get output something like this:

***** hash access
  before: {:password=>"too many secrets"}
  after: {:password=>"too many secrets"}
  backtrace:
    hash_spy.rb:27:in `print_password'
    hash_spy.rb:30
too many secrets
***** hash access
  before: {:password=>"too many secrets"}
  after: {:password=>"FAIL"}
  backtrace:
    hash_spy.rb:23:in `change_password'
    hash_spy.rb:31
***** hash access
  before: {:password=>"FAIL"}
  after: {:password=>"FAIL"}
  backtrace:
    hash_spy.rb:27:in `print_password'
    hash_spy.rb:32
FAIL

And by reading through the output, we can see that the second time the hash is accessed is when the value is changed, so the perpetrator is on line 23 of hash_spy.rb in the change_password method. Here's the entire script in one gist for reference.

Posted in Technology | Tags Ruby, Rails

Comments

1.

Curious to why you didn't monkey patch #[] to print the caller on invocation

# Posted By bryanl on Wednesday, February 24 2010 at 11:50 PM

2.

@bryanl, yes, but you'd also then need to patch Hash#merge!.

# Posted By Collin VanDyck on Thursday, February 25 2010 at 1:20 PM

Comments Disabled