This is the base of the instrumentation layer.
The idea is to allow instrumentation of code blocks.
Each time this code is executed an event is fired to some event
listeners that in turn can do whatever is needed to perform the instrumentation.

This patch adds:
 * code instrumentation calls
 * the listener system

Listeners are added by adding a file to puppet/util/instrumentation/listeners
containing:

Puppet::Util::Instrumentation.new_listener(:my_instrumentation, pattern) do

  def notify(label, event, data)
    ... do something for data...
  end
end

It is possible to use a "pattern". The listener will be notified only
if the pattern match the label of the event.
The pattern can be a symbol, a string or a regex.
If no pattern is provided, then the listener will be notified for every
event.

The notify method will be called before and after the insturmented code
is executed, with respectively an event of :start and then :stop.

This class is thread-safe.

Signed-off-by: Brice Figureau <brice-pup...@daysofwonder.com>
---
 lib/puppet/util/instrumentation.rb              |  163 +++++++++++++++++++++++
 lib/puppet/util/instrumentation/listener.rb     |   62 +++++++++
 spec/unit/util/instrumentation/listener_spec.rb |   88 ++++++++++++
 spec/unit/util/instrumentation_spec.rb          |  136 +++++++++++++++++++
 4 files changed, 449 insertions(+), 0 deletions(-)
 create mode 100644 lib/puppet/util/instrumentation.rb
 create mode 100644 lib/puppet/util/instrumentation/listener.rb
 create mode 100755 spec/unit/util/instrumentation/listener_spec.rb
 create mode 100755 spec/unit/util/instrumentation_spec.rb

diff --git a/lib/puppet/util/instrumentation.rb 
b/lib/puppet/util/instrumentation.rb
new file mode 100644
index 0000000..1b6abbb
--- /dev/null
+++ b/lib/puppet/util/instrumentation.rb
@@ -0,0 +1,163 @@
+require 'puppet'
+require 'puppet/util/classgen'
+require 'puppet/util/instance_loader'
+
+class Puppet::Util::Instrumentation
+  extend Puppet::Util::ClassGen
+  extend Puppet::Util::InstanceLoader
+  extend MonitorMixin
+
+  # we're using a ruby lazy autoloader to prevent a loop when requiring 
listeners
+  # since this class sets up an indirection, but this one is used in 
Indirection
+  autoload :Listener, 'puppet/util/instrumentation/listener'
+  autoload :Data, 'puppet/util/instrumentation/data'
+
+  # Set up autoloading and retrieving of instrumentation listeners.
+  instance_load :listener, 'puppet/util/instrumentation/listeners'
+
+  class << self
+    attr_accessor :listeners, :listeners_of
+  end
+
+  # instrumentation layer
+
+  # Triggers an instrumentation
+  #
+  # Call this method around the instrumentation point
+  #   Puppet::Util::Instrumentation.instrument(:my_long_computation) do
+  #     ... a long computation
+  #   end
+  #   
+  # This will send an event to all the listeners of "my_long_computation".
+  # The same can be achieved faster than using a block by calling start and 
stop.
+  # around the code to instrument.
+  def self.instrument(label, data = {})
+    id = self.start(label, data)
+    yield
+  ensure
+    self.stop(label, id, data)
+  end
+
+  # Triggers a "start" instrumentation event
+  # 
+  # Important note:
+  #  For proper use, the data hash instance used for start should also
+  #  be used when calling stop. The idea is to use the current scope
+  #  where start is called to retain a reference to 'data' so that it is 
possible
+  #  to send it back to stop.
+  #  This way listeners can match start and stop events more easily.
+  def self.start(label, data)
+    data[:started] = Time.now
+    publish(label, :start, data)
+    data[:id] = next_id
+  end
+
+  # Triggers a "stop" instrumentation event
+  def self.stop(label, id, data)
+    data[:finished] = Time.now
+    publish(label, :stop, data)
+  end
+
+  def self.publish(label, event, data)
+    listeners_of(label).each do |k,l|
+      l.notify(label, event, data)
+    end
+  end
+
+  def self.listeners
+    @listeners.values
+  end
+
+  def self.listeners_of(label)
+    synchronize {
+      @listeners_of[label] ||= @listeners.select do |k,l|
+        l.listen_to?(label)
+      end
+    }
+  end
+
+  # Adds a new listener
+  # 
+  # Usage:
+  #   Puppet::Util::Instrumentation.new_listener(:my_instrumentation, pattern) 
do
+  # 
+  #     def notify(label, data)
+  #       ... do something for data...
+  #     end
+  #   end
+  # 
+  # It is possible to use a "pattern". The listener will be notified only
+  # if the pattern match the label of the event.
+  # The pattern can be a symbol, a string or a regex.
+  # If no pattern is provided, then the listener will be called for every 
events
+  def self.new_listener(name, options = {}, &block)
+    Puppet.debug "new listener called #{name}"
+    name = symbolize(name)
+    listener = genclass(name, :hash => instance_hash(:listener), :block => 
block)
+    listener.send(:define_method, :name) do
+      name
+    end
+    subscribe(listener.new, options[:label_pattern], options[:event])
+  end
+
+  def self.subscribe(listener, label_pattern, event)
+    synchronize {
+      Puppet.debug "registering instrumentation listener #{listener.name}"
+      @listeners[listener.name] = Listener.new(listener, label_pattern, event)
+      rehash
+    }
+  end
+
+  def self.unsubscribe(listener)
+    synchronize {
+      Puppet.info "unregistering instrumentation listener #{listener.name}"
+      @listeners.delete(listener.name.to_s)
+      rehash
+    }
+  end
+
+  def self.init
+    synchronize {
+      @listeners ||= {}
+      @listeners_of ||= {}
+      instance_loader(:listener).loadall
+    }
+  end
+
+  def self.rehash
+    @listeners_of.clear
+  end
+
+  def self.clear
+    synchronize {
+      @listeners = {}
+      @listeners_of = {}
+      @id = 0
+    }
+  end
+
+  def self.[](key)
+    synchronize {
+      key = symbolize(key)
+      @listeners[key]
+    }
+  end
+
+  def self.[]=(key, value)
+    synchronize {
+      key = symbolize(key)
+      @listeners[key] = value
+      rehash
+    }
+  end
+
+  private
+
+  def self.next_id
+    synchronize {
+      id = @id || 0
+      @id = id + 1
+      id
+    }
+  end
+end
diff --git a/lib/puppet/util/instrumentation/listener.rb 
b/lib/puppet/util/instrumentation/listener.rb
new file mode 100644
index 0000000..51b844a
--- /dev/null
+++ b/lib/puppet/util/instrumentation/listener.rb
@@ -0,0 +1,62 @@
+require 'puppet/indirector'
+require 'puppet/util/instrumentation'
+require 'puppet/util/instrumentation/data'
+
+class Puppet::Util::Instrumentation::Listener
+  extend Puppet::Indirector
+
+  indirects :instrumentation_listener, :terminus_class => :local
+
+  attr_reader :pattern, :listener
+  attr_accessor :enabled
+
+  def initialize(listener, pattern = nil, enabled = false)
+    @pattern = pattern.is_a?(Symbol) ? pattern.to_s : pattern
+    raise "Listener isn't a correct listener (it doesn't provide the notify 
method)" unless listener.respond_to?(:notify)
+    @listener = listener
+    @enabled = enabled
+  end
+
+  def notify(label, event, data)
+    listener.notify(label, event, data)
+  rescue => e
+    Puppet.warnonce "Error during instrumentation notification: #{e}"
+  end
+
+  def listen_to?(label)
+    enabled? and (!@pattern || @pattern === label.to_s)
+  end
+
+  def enabled?
+    !!@enabled
+  end
+
+  def same?(delegate)
+    delegate == @listener
+  end
+
+  def name
+    @listener.name
+  end
+
+  def data
+    { :data => @listener.data }
+  end
+
+  def to_pson(*args)
+    result = {
+      :document_type => "Puppet::Util::Instrumentation::Listener",
+      :data => {
+        :name => name,
+        :pattern => pattern,
+        :enabled => enabled?
+      }
+    }
+    result.to_pson(*args)
+  end
+
+  def self.from_pson(data)
+    result = Puppet::Util::Instrumentation[data["name"]]
+    self.new(result.listener, result.pattern, data["enabled"])
+  end
+end
diff --git a/spec/unit/util/instrumentation/listener_spec.rb 
b/spec/unit/util/instrumentation/listener_spec.rb
new file mode 100755
index 0000000..ca4105d
--- /dev/null
+++ b/spec/unit/util/instrumentation/listener_spec.rb
@@ -0,0 +1,88 @@
+#!/usr/bin/env rspec
+
+require 'spec_helper'
+require 'matchers/json'
+
+require 'puppet/util/instrumentation'
+require 'puppet/util/instrumentation/listener'
+
+describe Puppet::Util::Instrumentation::Listener do
+
+  Listener = Puppet::Util::Instrumentation::Listener
+
+  before(:each) do
+    @delegate = stub 'listener', :notify => nil, :name => 'listener'
+    @listener = Listener.new(@delegate)
+    @listener.enabled = true
+  end
+
+  it "should indirect instrumentation_listener" do
+    Listener.indirection.name.should == :instrumentation_listener
+  end
+
+  it "should raise an error if delegate doesn't support notify" do
+    lambda { Listener.new(Object.new) }.should raise_error
+  end
+
+  it "should not be enabled by default" do
+    Listener.new(@delegate).should_not be_enabled
+  end
+
+  it "should delegate notification" do
+    @delegate.expects(:notify).with(:event, :start, {})
+    listener = Listener.new(@delegate)
+    listener.notify(:event, :start, {})
+  end
+
+  it "should not listen is not enabled" do
+    @listener.enabled = false
+    @listener.should_not be_listen_to(:label)
+  end
+
+  it "should listen to all label if created without pattern" do
+    @listener.should be_listen_to(:improbable_label)
+  end
+
+  it "should listen to specific string pattern" do
+    listener = Listener.new(@delegate, "specific")
+    listener.enabled = true
+    listener.should be_listen_to(:specific)
+  end
+
+  it "should listen to specific regex pattern" do
+    listener = Listener.new(@delegate, /spe.*/)
+    listener.enabled = true
+    listener.should be_listen_to(:specific_pattern)
+  end
+
+  it "should delegate its name to the underlying listener" do
+    @delegate.expects(:name).returns("myname")
+    @listener.name.should == "myname"
+  end
+
+  it "should delegate data fetching to the underlying listener" do
+    @delegate.expects(:data).returns(:data)
+    @listener.data.should == {:data => :data }
+  end
+
+  describe "when serializing to pson" do
+    it "should return a pson object containing pattern, name and status" do
+      @listener.should set_json_attribute('enabled').to(true)
+      @listener.should set_json_attribute('name').to("listener")
+    end
+  end
+
+  describe "when deserializing from pson" do
+    it "should lookup the archetype listener from the instrumentation layer" do
+      
Puppet::Util::Instrumentation.expects(:[]).with("listener").returns(@listener)
+      Puppet::Util::Instrumentation::Listener.from_pson({"name" => "listener"})
+    end
+
+    it "should create a new listener shell instance delegating to the 
archetypal listener" do
+      
Puppet::Util::Instrumentation.expects(:[]).with("listener").returns(@listener)
+      @listener.stubs(:listener).returns(@delegate)
+      Puppet::Util::Instrumentation::Listener.expects(:new).with(@delegate, 
nil, true)
+      Puppet::Util::Instrumentation::Listener.from_pson({"name" => "listener", 
"enabled" => true})
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/unit/util/instrumentation_spec.rb 
b/spec/unit/util/instrumentation_spec.rb
new file mode 100755
index 0000000..9c214bc
--- /dev/null
+++ b/spec/unit/util/instrumentation_spec.rb
@@ -0,0 +1,136 @@
+#!/usr/bin/env rspec
+
+require 'spec_helper'
+
+require 'puppet/util/instrumentation'
+
+describe Puppet::Util::Instrumentation do
+
+  Instrumentation = Puppet::Util::Instrumentation
+
+  after(:each) do
+    Instrumentation.clear
+  end
+
+  it "should instance-load instrumentation listeners" do
+    Instrumentation.instance_loader(:listener).should 
be_instance_of(Puppet::Util::Autoload)
+  end
+
+  it "should have a method for registering instrumentation listeners" do
+    Instrumentation.should respond_to(:new_listener)
+  end
+
+  it "should have a method for retrieving instrumentation listener by name" do
+    Instrumentation.should respond_to(:listener)
+  end
+
+  describe "when registering listeners" do
+    it "should evaluate the supplied block as code for a class" do
+      Instrumentation.expects(:genclass).returns(Class.new { def notify(label, 
event, data) ; end })
+      Instrumentation.new_listener(:testing, :label_pattern => 
:for_this_label, :event => :all) { }
+    end
+
+    it "should subscribe a new listener instance" do
+      Instrumentation.expects(:genclass).returns(Class.new { def notify(label, 
event, data) ; end })
+      Instrumentation.new_listener(:testing, :label_pattern => 
:for_this_label, :event => :all) { }
+      Instrumentation.listeners.size.should == 1
+      Instrumentation.listeners[0].pattern.should == "for_this_label"
+    end
+
+    it "should be possible to access listeners by name" do
+      Instrumentation.expects(:genclass).returns(Class.new { def notify(label, 
event, data) ; end })
+      Instrumentation.new_listener(:testing, :label_pattern => 
:for_this_label, :event => :all) { }
+      Instrumentation["testing"].should_not be_nil
+    end
+
+    it "should be possible to store a new listener by name" do
+      listener = stub 'listener'
+      Instrumentation["testing"] = listener
+      Instrumentation["testing"].should == listener
+    end
+  end
+
+  describe "when unsubscribing listener" do
+    it "should remove it from the listeners" do
+      listener = stub 'listener', :notify => nil, :name => "mylistener"
+      Instrumentation.subscribe(listener, :for_this_label, :all)
+      Instrumentation.unsubscribe(listener)
+      Instrumentation.listeners.size.should == 0
+    end
+  end
+
+  describe "when firing events" do
+    it "should be able to find all listeners matching a label" do
+      listener = stub 'listener', :notify => nil, :name => "mylistener"
+      Instrumentation.subscribe(listener, :for_this_label, :all)
+      Instrumentation.listeners[0].enabled = true
+
+      Instrumentation.listeners_of(:for_this_label).size.should == 1
+    end
+
+    it "should fire events to matching listeners" do
+      listener = stub 'listener', :notify => nil, :name => "mylistener"
+      Instrumentation.subscribe(listener, :for_this_label, :all)
+      Instrumentation.listeners[0].enabled = true
+
+      listener.expects(:notify).with(:for_this_label, :start, {})
+
+      Instrumentation.publish(:for_this_label, :start, {})
+    end
+  end
+
+  describe "when instrumenting code" do
+    before(:each) do
+      Instrumentation.stubs(:publish)
+    end
+    describe "with a block" do
+      it "should execute it" do
+        executed = false
+        Instrumentation.instrument(:event) do
+          executed = true
+        end
+        executed.should be_true
+      end
+
+      it "should publish an event before execution" do
+        Instrumentation.expects(:publish).with { |label,event,data| label == 
:event && event == :start }
+        Instrumentation.instrument(:event) {}
+      end
+
+      it "should publish an event after execution" do
+        Instrumentation.expects(:publish).with { |label,event,data| label == 
:event && event == :stop }
+        Instrumentation.instrument(:event) {}
+      end
+
+      it "should publish the event even when block raised an exception" do
+        Instrumentation.expects(:publish).with { |label,event,data| label == 
:event }
+        lambda { Instrumentation.instrument(:event) { raise "not working" } 
}.should raise_error
+      end
+
+      it "should retain start end finish time of the event" do
+        Instrumentation.expects(:publish).with { |label,event,data| 
data.include?(:started) and data.include?(:finished) }
+        Instrumentation.instrument(:event) {}
+      end
+    end
+
+    describe "without a block" do
+      it "should raise an error if stop is called with no matching start" do
+        lambda{ Instrumentation.stop(:event) }.should raise_error
+      end
+
+      it "should publish an event on stop" do
+        Instrumentation.expects(:publish).with { |label,event,data| event == 
:start }
+        Instrumentation.expects(:publish).with { |label,event,data| event == 
:stop and data.include?(:started) and data.include?(:finished) }
+        data = {}
+        Instrumentation.start(:event, data)
+        Instrumentation.stop(:event, 1, data)
+      end
+
+      it "should return a different id per event" do
+        data = {}
+        Instrumentation.start(:event, data).should == 0
+        Instrumentation.start(:event, data).should == 1
+      end
+    end
+  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