Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cross-cutting logging in Ruby

I'm trying to add logging to a method from the outside (Aspect-oriented-style)

class A
  def test
    puts "I'm Doing something..."
  end
end

class A # with logging!
  alias_method :test_orig, :test
  def test
    puts "Log Message!"
    test_orig
  end
end

a = A.new
a.test

The above works alright, except that if I ever needed to do alias the method again, it goes into an infinite loop. I want something more like super, where I could extend it as many times as I needed, and each extension with alias its parent.

like image 717
Sean Clark Hess Avatar asked Dec 22 '22 06:12

Sean Clark Hess


2 Answers

Another alternative is to use unbound methods:

class A
  original_test = instance_method(:test)
  define_method(:test) do
    puts "Log Message!"
    original_test.bind(self).call
  end
end

class A
  original_test = instance_method(:test)
  counter = 0
  define_method(:test) do
    counter += 1
    puts "Counter = #{counter}"
    original_test.bind(self).call
  end
end

irb> A.new.test
Counter = 1
Log Message!
#=> #....
irb> A.new.test
Counter = 2
Log Message!
#=> #.....

This has the advantage that it doesn't pollute the namespace with additional method names, and is fairly easily abstracted, if you want to make a class method add_logging or what have you.

class Module
  def add_logging(*method_names)
    method_names.each do |method_name|
      original_method = instance_method(method_name)
      define_method(method_name) do |*args,&blk|
        puts "logging #{method_name}"
        original_method.bind(self).call(*args,&blk)
      end
    end
  end
end

class A
  add_logging :test
end

Or, if you wanted to be able to do a bunch of aspects w/o a lot of boiler plate, you could write a method that writes aspect-adding methods!

class Module
  def self.define_aspect(aspect_name, &definition)
    define_method(:"add_#{aspect_name}") do |*method_names|
      method_names.each do |method_name|
        original_method = instance_method(method_name)
        define_method(method_name, &(definition[method_name, original_method]))
      end
    end
  end
  # make an add_logging method
  define_aspect :logging do |method_name, original_method|
    lambda do |*args, &blk|
      puts "Logging #{method_name}"
      original_method.bind(self).call(*args, &blk)
    end
  end
  # make an add_counting method
  global_counter = 0
  define_aspect :counting do |method_name, original_method|
     local_counter = 0
     lambda do |*args, &blk|
       global_counter += 1
       local_counter += 1
       puts "Counters: global@#{global_counter}, local@#{local_counter}"
       original_method.bind(self).call(*args, &blk)
     end
  end      
end

class A
  def test 
    puts "I'm Doing something..." 
  end
  def test1 
    puts "I'm Doing something once..." 
  end
  def test2
    puts "I'm Doing something twice..." 
    puts "I'm Doing something twice..." 
  end
  def test3
    puts "I'm Doing something thrice..." 
    puts "I'm Doing something thrice..." 
    puts "I'm Doing something thrice..." 
  end
  def other_tests
    puts "I'm Doing something else..." 
  end

  add_logging :test, :test2, :test3
  add_counting :other_tests, :test1, :test3
end
like image 198
rampion Avatar answered Dec 24 '22 20:12

rampion


First choice: subclass instead of overriding:

class AWithLogging < A\
  def test
    puts "Log Message!"
    super
  end
end

Second choice: name your orig methods more carefully:

class A # with logging!
  alias_method :test_without_logging, :test
  def test
    puts "Log Message!"
    test_without_logging
  end
end

Then another aspect uses a different orig name:

class A # with frobnication!
  alias_method :test_without_frobnication, :test
  def test
    Frobnitz.frobnicate(self)
    test_without_frobnication
  end
end
like image 32
Grandpa Avatar answered Dec 24 '22 20:12

Grandpa