Ruby Method Auditing Using Module#prepend

Jem Zornow
4 min readOct 18, 2018

--

Yesterday I was interviewing for a Ruby developer position at a company here in Denver when my interviewer posed a good metaprogramming challenge: he asked me to write an automated method auditor.

He described a class method that would take the name of an instance method and generate an automatic, logged paper trail any time that target method is called. Something like this:

That would produce output that looked like this:

I came up with a solution that worked but involved some potentially dangerous hacks to Object#send. Before presenting my solution, I said “this is probably a bad idea, but…” and I was right. Good ideas rarely start out that way.

The solution that he proposed involved using the often-overlooked Module#prepend and I thought it was cool enough to warrant a write-up.

Module Magic

The strategy was to use the behavior of Module#prepend to create a dynamically-generated wrapper method for Messenger#share.

In that wrapper, we’ll have one “Performing” message fire off before the body of the method is executed, the method execution itself, and then a final “Exiting” message once the method’s execution has completed successfully.

But to understand how to use Module#prepend to do this, we first need a good understanding of how method inheritance works in Ruby.

Ancestor Chains

Each Ruby object sits at the end of what’s called an Ancestor Chain. It’s a list of the ancestors objects that the object inherits from. Ruby uses this Ancestor Chain to determine which version of a method (if any at all) is executed when the object receives a message.

You can actually view the ancestor tree for any object by calling Module#ancestors. For example, here’s the ancestor chain for our Messenger class:

When we #include or #prepend a module into a class, Ruby makes changes to that classes’ ancestor chain, tweaking which methods are found and in what order. The big difference between #include and #prepend is where that change is made. Let’s create a module called Auditable which will (eventually) hold the code that does our auditing magic:

If we use Module#include to import the (currently nonexistent) methods from Auditable, Ruby will squeeze that module in as an ancestor of our Messenger class. Here, see for yourself:

When we call a method on Messenger, Ruby will look at what’s defined in the Messenger class and — if it can’t find a method that matches the call — climb up the ancestor chain to Auditable to look again. If it doesn’t find what it’s looking for on Auditable it moves on to Object and so forth.

That’s #include. If we instead use Module#prepend to import the contents of the module, we get a totally different effect:

#prepend makes Messenger an ancestor of Auditable. Wait. What?

This might look like Messenger is now a superclass of Auditable, but that’s not exactly what’s happening here. Instances of class Messenger are still instances of class Messenger but Ruby will now look for methods to use for Messenger in the Auditable module before looking for them on the Messenger class itself.

And that, friends, is what we’ll be taking advantage of to build this auditor: if we create a method called Auditable#share, Ruby will find that before it finds Messenger#share. We can then use a super call in Auditable#share to access (and execute!) the original method defined on Messenger.

The Module Itself

We’re not actually going to create a method called Auditable#share. Why not? Because we want this to be a flexible utility. If we hard-code the method Auditable#share, we’ll be able to use it only on methods that implement the #share method. Or worse, we’d have to re-implement this same auditor pattern for every method we ever want to audit. No thanks.

So instead, we’re going to define our method dynamically and the audit_method class method to fire it off:

When audit_method is called in an implementing class, a method is created in the Auditable module with the same name as the method-to-audit. In our case, it will create a method called Auditable#share. As mentioned, Ruby will find this method before it finds the original method on Messenger because we’re prepending the Auditable module in the implementing class.

That means that we can use a super call to reach up the ancestor chain and execute Messenger#send. When we do so, we pass the arguments we’ve collected (*arguments) up the chain too.

Once we’ve called the original method, we print our exit message and call it a day. Good work, gang!

Bringing it all Together

Now it’s just a matter of prepending this module to our Messenger class and we should be good to go:

And good golly it works:

The implications here are huge for auditing, but there’s more to this trick. You can used prepending to change the behavior of objects without changing the objects themselves. This is an adaptable and clear way to create higher-order components in Ruby. Performance testing, error handling. You can do a lot here.

Conclusion

Ruby modules are more complicated than most people think. It’s not as simple as “dumping” code from one place to another, and understanding those differences unlocks some really neat tools for your utility belt.

You may have noticed that I only talked about Module#include and Module#prepend today, and that I didn’t touch on Module#extend. That’s because it works very differently than its cousins. I’ll write up an in-depth explanation of Module#extend soon to complete the set.

For now, if you want to learn more I’d recommend reading Ruby modules: Include vs Prepend vs Extend by Léonard Hetsch. It was really helpful in putting all of this together.

--

--

Jem Zornow

Rubyist, Writer, Musician, Educator, Bread Enthusiast