This patch contains a module (Instrumentable) that can be mixed in
various puppet classes to help define probes.

To add a probe for a given method, it is just a matter of using
the Instrumentable probe method:

class MyClass
  extend Instrumentable

  probe :thismethod
  def thismethod(arg)
  ...
  end
end

Used with no options the probe is created with a default label of
the instrumented method name and an empty dataset.

It is possible to set a given label, or use a proc as the label, in which
case the label will be the result of the evaluation of the said proc.

For instance:

class MyClass
  extend Instrumentable

  probe :thismethod, :label => Proc.new { |parent,args| args[0] }

  def thismethod(arg1, arg2)
  end
end
In this last example, the label will be the value of "arg1".
The parent argument of the :label block will contain the "self"
value of the instrumented method

When the thismethod will be called, it will in turn call the instrumentation
layer with the given label and data.

Signed-off-by: Brice Figureau <brice-pup...@daysofwonder.com>
---
 lib/puppet/util/instrumentation/Instrumentable.rb  |  139 ++++++++++++++++
 .../util/instrumentation/instrumentable_spec.rb    |  173 ++++++++++++++++++++
 2 files changed, 312 insertions(+), 0 deletions(-)
 create mode 100644 lib/puppet/util/instrumentation/Instrumentable.rb
 create mode 100755 spec/unit/util/instrumentation/instrumentable_spec.rb

diff --git a/lib/puppet/util/instrumentation/Instrumentable.rb 
b/lib/puppet/util/instrumentation/Instrumentable.rb
new file mode 100644
index 0000000..fdec17a
--- /dev/null
+++ b/lib/puppet/util/instrumentation/Instrumentable.rb
@@ -0,0 +1,139 @@
+require 'monitor'
+require 'puppet/util/instrumentation'
+
+# This is the central point of all declared probes.
+# Every class needed to declare probes should include this module
+# and declare the methods that are subject to instrumentation:
+# 
+#   class MyClass
+#     extend Puppet::Util::Instrumentation::Instrumentable
+#     
+#     probe :mymethod
+#     
+#     def mymethod
+#       ... this is code to be instrumented ...
+#     end
+#   end
+module Puppet::Util::Instrumentation::Instrumentable
+  INSTRUMENTED_CLASSES = {}.extend(MonitorMixin)
+
+  attr_reader :probes
+
+  class Probe
+    attr_reader :klass, :method, :label, :data
+
+    def initialize(method, klass, options = {})
+      @method = method
+      @klass = klass
+
+      @label = options[:label] || method
+      @data = options[:data] || {}
+    end
+
+    def enable
+      raise "Probe already enabled" if enabled?
+      method = @method
+      label = @label
+      data = @data
+      klass.class_eval {
+        alias_method("instrumented_#{method}", method)
+        define_method(method) do |*args|
+          id = nil
+          instrumentation_data = nil
+          begin
+            instrumentation_label = label.respond_to?(:call) ? 
label.call(self, args) : label
+            instrumentation_data = data.respond_to?(:call) ? data.call(self, 
args) : data
+            id = Puppet::Util::Instrumentation.start(instrumentation_label, 
instrumentation_data)
+            send("instrumented_#{method}".to_sym, *args)
+          ensure
+            Puppet::Util::Instrumentation.stop(instrumentation_label, id, 
instrumentation_data || {})
+          end
+        end
+      }
+      @enabled = true
+    end
+
+    def disable
+      raise "Probe is not enabled" unless enabled?
+      method = @method
+      label = @label
+      data = @data
+      klass.class_eval do
+        alias_method("instrumented_#{method}", method)
+        remove_method("instrumented_#{method}".to_sym)
+      end
+      @enabled = false
+    end
+
+    def enabled?
+      !!@enabled
+    end
+  end
+
+  # Declares a new probe
+  # 
+  # It is possible to pass several options that will be later on evaluated
+  # and sent to the instrumentation layer.
+  # 
+  # label::
+  #   this can either be a static symbol/string or a block. If it's a block
+  #   this one will be evaluated on every call of the instrumented method and
+  #   should return a string or a symbol
+  #   
+  # data::
+  #   this can be a hash or a block. If it's a block this one will be evaluated
+  #   on every call of the instrumented method and should return a hash.
+  # 
+  # Example:
+  #   
+  #   class MyClass
+  #     extend Instrumentable
+  #     
+  #     probe :mymethod, :data => Proc.new { |args|  { :data => args[1] } }, 
:label => Proc.new { |args| args[0] }
+  #     
+  #     def mymethod(name, options)
+  #     end
+  #     
+  #   end
+  # 
+  def probe(method, options = {})
+    INSTRUMENTED_CLASSES.synchronize {
+      (@probes ||= []) << Probe.new(method, self, options)
+      INSTRUMENTED_CLASSES[self] = @probes
+    }
+  end
+
+  def self.probes
+    @probes
+  end
+
+  def self.probe_names
+    probe_names = []
+    each_probes { |probe| probe_names << "#{probe.klass}.#{probe.method}" }
+    probe_names
+  end
+
+  def self.enable_probes
+    each_probes { |probe| probe.enable }
+  end
+
+  def self.disable_probes
+    each_probes { |probe| probe.disable }
+  end
+
+  def self.clear_probes
+    INSTRUMENTED_CLASSES.synchronize {
+      INSTRUMENTED_CLASSES.clear
+    }
+    nil # do not leak our probes to the exterior world
+  end
+
+  def self.each_probes
+    INSTRUMENTED_CLASSES.synchronize {
+      INSTRUMENTED_CLASSES.each_key do |klass|
+        klass.probes.each { |probe| yield probe }
+      end
+    }
+    nil # do not leak our probes to the exterior world
+  end
+end
\ No newline at end of file
diff --git a/spec/unit/util/instrumentation/instrumentable_spec.rb 
b/spec/unit/util/instrumentation/instrumentable_spec.rb
new file mode 100755
index 0000000..50cbcb4
--- /dev/null
+++ b/spec/unit/util/instrumentation/instrumentable_spec.rb
@@ -0,0 +1,173 @@
+#!/usr/bin/env rspec
+
+require 'spec_helper'
+
+require 'puppet/util/instrumentation'
+require 'puppet/util/instrumentation/instrumentable'
+
+describe Puppet::Util::Instrumentation::Instrumentable::Probe do
+
+  before(:each) do
+    Puppet::Util::Instrumentation.stubs(:start)
+    Puppet::Util::Instrumentation.stubs(:stop)
+
+    class ProbeTest
+      def mymethod(arg1, arg2, arg3)
+      end
+    end
+  end
+
+  after(:each) do
+    if ProbeTest.method_defined?(:instrumented_mymethod)
+      ProbeTest.class_eval {
+        remove_method(:mymethod)
+        alias_method(:mymethod, :instrumented_mymethod)
+      }
+    end
+    Puppet::Util::Instrumentation::Instrumentable.clear_probes
+  end
+
+  describe "when enabling a probe" do
+    it "should raise an error if the probe is already enabled" do
+      probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+      probe.enable
+      lambda { probe.enable }.should raise_error
+    end
+
+    it "should rename the original method name" do
+      probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+      probe.enable
+      ProbeTest.new.should respond_to(:instrumented_mymethod)
+    end
+
+    it "should create a new method of the original name" do
+      probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+      probe.enable
+      ProbeTest.new.should respond_to(:mymethod)
+    end
+  end
+
+  describe "when disabling a probe" do
+    it "should raise an error if the probe is already enabled" do
+      probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+      lambda { probe.disable }.should raise_error
+    end
+
+    it "should rename the original method name" do
+      probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+      probe.enable
+      probe.disable
+      ProbeTest.new.should respond_to(:mymethod)
+    end
+
+    it "should remove the create method" do
+      probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+      probe.enable
+      probe.disable
+      ProbeTest.new.should_not respond_to(:instrumented_mymethod)
+    end
+  end
+
+  describe "when a probe is called" do
+    it "should call the original method" do
+      probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+      probe.enable
+      test = ProbeTest.new
+      test.expects(:instrumented_mymethod).with(1,2,3)
+      test.mymethod(1,2,3)
+    end
+
+    it "should start the instrumentation" do
+      Puppet::Util::Instrumentation.expects(:start)
+      probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+      probe.enable
+      test = ProbeTest.new
+      test.mymethod(1,2,3)
+    end
+
+    it "should stop the instrumentation" do
+      Puppet::Util::Instrumentation.expects(:stop)
+      probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+      probe.enable
+      test = ProbeTest.new
+      test.mymethod(1,2,3)
+    end
+
+    describe "and the original method raises an exception" do
+      it "should propagate the exception" do
+        probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
+        probe.enable
+        test = ProbeTest.new
+        test.expects(:instrumented_mymethod).with(1,2,3).raises
+        lambda { test.mymethod(1,2,3) }.should raise_error
+      end
+    end
+
+    describe "with a static label" do
+      it "should send the label to the instrumentation layer" do
+        probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, 
:label => :mylabel)
+        probe.enable
+        test = ProbeTest.new
+        Puppet::Util::Instrumentation.expects(:start).with { |label,data| 
label == :mylabel }.returns(42)
+        Puppet::Util::Instrumentation.expects(:stop).with(:mylabel, 42, {})
+        test.mymethod(1,2,3)
+      end
+    end
+
+    describe "with a dynamic label" do
+      it "should send the evaluated label to the instrumentation layer" do
+        probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, 
:label => Proc.new { |parent,args| "dynamic#{args[0]}" } )
+        probe.enable
+        test = ProbeTest.new
+        Puppet::Util::Instrumentation.expects(:start).with { |label,data| 
label == "dynamic1" }.returns(42)
+        Puppet::Util::Instrumentation.expects(:stop).with("dynamic1",42,{})
+        test.mymethod(1,2,3)
+      end
+    end
+
+    describe "with static data" do
+      it "should send the data to the instrumentation layer" do
+        probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, 
:data => { :static_data => "nothing" })
+        probe.enable
+        test = ProbeTest.new
+        Puppet::Util::Instrumentation.expects(:start).with { |label,data| data 
== { :static_data => "nothing" }}
+        test.mymethod(1,2,3)
+      end
+    end
+
+    describe "with dynamic data" do
+      it "should send the evaluated label to the instrumentation layer" do
+        probe = 
Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, 
:data => Proc.new { |parent, args| { :key => args[0] }  } )
+        probe.enable
+        test = ProbeTest.new
+        Puppet::Util::Instrumentation.expects(:start).with { |label,data| data 
== { :key => 1 } }
+        Puppet::Util::Instrumentation.expects(:stop)
+        test.mymethod(1,2,3)
+      end
+    end
+  end
+end
+
+describe Puppet::Util::Instrumentation::Instrumentable do
+  before(:each) do
+    class ProbeTest2
+      extend Puppet::Util::Instrumentation::Instrumentable
+      probe :mymethod
+      def mymethod(arg1,arg2,arg3)
+      end
+    end
+  end
+
+  after do
+    Puppet::Util::Instrumentation::Instrumentable.clear_probes
+  end
+
+  it "should allow probe definition" do
+    Puppet::Util::Instrumentation::Instrumentable.probe_names.should 
be_include("ProbeTest2.mymethod")
+  end
+
+  it "should be able to enable all probes" do
+    Puppet::Util::Instrumentation::Instrumentable.enable_probes
+    ProbeTest2.new.should respond_to(:instrumented_mymethod)
+  end
+end
\ No newline at end of file
-- 
1.7.5.1

-- 
You received this message because you are subscribed to the Google Groups 
"Puppet Developers" group.
To post to this group, send email to puppet-dev@googlegroups.com.
To unsubscribe from this group, send email to 
puppet-dev+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/puppet-dev?hl=en.

Reply via email to