3

I have classes with methods that log when a method is entered and exited like so:
def methodName1(args) @logger.debug(">>#{callee}") ... @logger.debug("<<#{callee}") end

def methodName2(args) @logger.debug(">>#{callee}") ... @logger.debug("<<#{callee}") end

I was wondering if there was a metaprogramming way to surround the methods with the logger calls? It would involve identifying all the methods in a class that I want surrounded first and then surrounding them.

A

amadain
  • 2,724
  • 4
  • 37
  • 58

4 Answers4

3

I would be inclined to prepend to the class a dynamically-created anonymous module whose instance methods use super to call an instance method of the class of the same name, after printing a method-entry message and before printing a method-exit message.

Let's begin by creating a class with two instance methods, one passed a block when involked.

class C
  def mouse(nbr_mice, impairment)
    puts "%d %s mice" % [nbr_mice, impairment]
  end
  def hubbard(*args)
    puts yield args
  end
end

C.ancestors
  #=> [C, Object, Kernel, BasicObject]
c = C.new
c.mouse(3, 'blind')
  # 3 blind mice
c.hubbard('old', 'mother', 'hubbard') { |a| a.map(&:upcase).join(' ') }
  # OLD MOTHER HUBBARD

Now we construct a method that creates the anonymous module and prepends it to the class.

def loggem(klass, *methods_to_log)
  log_mod = Module.new do
    code = methods_to_log.each_with_object('') { |m,str| str <<
      "def #{m}(*args); puts \"entering #{m}\"; super; puts \"leaving #{m}\"; end\n" }
    class_eval code
  end
  klass.prepend(log_mod)
end

We are now ready to invoke this method with the arguments equal to the class to which the module is to be prepended and the instance methods of that class that are to be logged.

loggem(C, :mouse, :hubbard)

C.ancestors
  #=> [#<Module:0x007fedab9ccf48>, C, Object, Kernel, BasicObject] 

c = C.new
c.method(:mouse).owner
  #=> #<Module:0x007fedab9ccf48> 
c.method(:mouse).super_method
  #=> #<Method: Object(C)#mouse> 
c.method(:hubbard).owner
  #=> #<Module:0x007fedab9ccf48> 
c.method(:hubbard).super_method
  #=> #<Method: Object(C)#hubbard> 

c.mouse(3, 'blind')
  # entering mouse
  # 3 blind mice
  # leaving mouse
c.hubbard('old', 'mother', 'hubbard') { |a| a.map(&:upcase).join(' ') }
  # entering hubbard
  # OLD MOTHER HUBBARD
  #leaving hubbard

See Module::new and Module#prepend.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1

You could use an around alias. Alias the original method, and then redefine it with the extra code:

alias_method :bare_methodname1, :methodname1

def methodname1(*args)
  @logger.debug(">>#{callee}")
  result = bare_methodname1(*args)
  @logger.debug("<<#{callee}")
  result
end

This isn't much different from what you've got now, but when you combine it with an array of method names, you get more of what you want:

method_names_ary.each do |name|
  alias_method "bare_" + name, name
  define_method(name) do |*args|
    @logger.debug(">>#{callee}")
    result = send("bare_" + name, *args)
    @logger.debug("<<#{callee}")
    result
  end
end

Put this in the target class outside of any method and it should redefine all the methods in your array to have the extra code you want.

David Stanley
  • 333
  • 3
  • 12
1

i guess this solution should help:

class Foo
    def initialize
        (self.methods - Object.methods).each do |method|
            # we need to make alias for method
            self.class.send(:alias_method, "#{method}_without_callback", method)
            # save method params, and destroy old method
            params = self.method(method).parameters.map(&:last).join(',')
            self.class.send(:undef_method,  method) 
            # creating new method with old name, and pass params to this
            eval("
            self.class.send(:define_method, method) do |#{params}|
                puts 'Call_before'
                self.send('#{method}_without_callback', #{params})
                puts 'Call_after'
            end
            ")
        end
    end
    def say_without_param
        puts "Hello!"
    end
    def say_hi(par1)
        puts "Hi, #{par1}"
    end

    def say_good_bye(par1, par2)
        puts "Good bye, #{par1} #{par2}"
    end
end

So when we create an object, after initialization there will be created alias methods, destroyed old methods, and new methods with call_backs will be created;

Usage example:

obj = Foo.new

obj.say_without_param # => Call_before
                           Hello!
                           Call_after

obj.say_hi('Johny') # =>   Call_before
                           Hi, Johny
                           Call_after

obj.say_good_bye('mr.', 'Smith') => Call_before
                                    Good bye, mr. Smith
                                    Call_after
Dmitry Cat
  • 475
  • 3
  • 11
  • I believe the line ```self.class.send(:define_method, method) do |#{params}|``` should be ```self.class.send(:define_method, '#{method}') do |#{params}|``` – Jonathan Branam Apr 23 '19 at 17:36
1

You could create a class method analogous to def that adds the watchers for you. This would change the method definition syntax a bit but might make for more readable code.

ie

module MethodLogging
  def log_def(method_name, &definition)
    define_method(method_name) do |*args|
      @logger.debug(">>#{__callee__}")
      definition.call(*args)
      @logger.debug("<<#{__callee__}")
    end
  end
end

class MyClass
  extend MethodLogging

  def initialize
    # make sure class has @logger defined, or else include it in some way in the MethodLogging module
    @logger = Logger.new(STDOUT)
  end

  def regular_method(x)
    puts x
  end

  log_def :logged_method do |x|
    puts x
  end
end

instance = MyClass.new
instance.regular_method(3)
# hello
instance.logged_method(3)
# D, [2017-03-22T14:59:18.889285 #58206] DEBUG -- : >>logged_method
# world
# D, [2017-03-22T14:59:18.889440 #58206] DEBUG -- : <<logged_method

Aside from the new method definition syntax, there's a minor downside where you get weird behavior if you don't respect the method's arity. Neither instance.logged_method() and instance.logged_method('hello', 'world') will throw an error with this method.

Glyoko
  • 2,071
  • 1
  • 14
  • 28