diff --git a/lib/puppet/indirector/instrumentation_data.rb b/lib/puppet/indirector/instrumentation_data.rb new file mode 100644 index 000000000..f1bea330d --- /dev/null +++ b/lib/puppet/indirector/instrumentation_data.rb @@ -0,0 +1,3 @@ +# A stub class, so our constants work. +class Puppet::Indirector::InstrumentationData +end diff --git a/lib/puppet/indirector/instrumentation_data/local.rb b/lib/puppet/indirector/instrumentation_data/local.rb new file mode 100644 index 000000000..6510d7f2d --- /dev/null +++ b/lib/puppet/indirector/instrumentation_data/local.rb @@ -0,0 +1,19 @@ +require 'puppet/indirector/instrumentation_data' + +class Puppet::Indirector::InstrumentationData::Local < Puppet::Indirector::Code + def find(request) + model.new(request.key) + end + + def search(request) + raise Puppet::DevError, "You cannot search for instrumentation data" + end + + def save(request) + raise Puppet::DevError, "You cannot save instrumentation data" + end + + def destroy(request) + raise Puppet::DevError, "You cannot remove instrumentation data" + end +end diff --git a/lib/puppet/indirector/instrumentation_data/rest.rb b/lib/puppet/indirector/instrumentation_data/rest.rb new file mode 100644 index 000000000..28b211781 --- /dev/null +++ b/lib/puppet/indirector/instrumentation_data/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/rest' +require 'puppet/indirector/instrumentation_data' + +class Puppet::Indirector::InstrumentationData::Rest < Puppet::Indirector::REST +end diff --git a/lib/puppet/indirector/instrumentation_listener.rb b/lib/puppet/indirector/instrumentation_listener.rb new file mode 100644 index 000000000..6aaa71ce6 --- /dev/null +++ b/lib/puppet/indirector/instrumentation_listener.rb @@ -0,0 +1,3 @@ +# A stub class, so our constants work. +class Puppet::Indirector::InstrumentationListener +end diff --git a/lib/puppet/indirector/instrumentation_listener/local.rb b/lib/puppet/indirector/instrumentation_listener/local.rb new file mode 100644 index 000000000..72578014c --- /dev/null +++ b/lib/puppet/indirector/instrumentation_listener/local.rb @@ -0,0 +1,23 @@ +require 'puppet/indirector/instrumentation_listener' + +class Puppet::Indirector::InstrumentationListener::Local < Puppet::Indirector::Code + def find(request) + Puppet::Util::Instrumentation[request.key] + end + + def search(request) + Puppet::Util::Instrumentation.listeners + end + + def save(request) + res = request.instance + Puppet::Util::Instrumentation[res.name] = res + nil # don't leak the listener + end + + def destroy(request) + listener = Puppet::Util::Instrumentation[request.key] + raise "Listener #{request.key} hasn't been subscribed" unless listener + Puppet::Util::Instrumentation.unsubscribe(listener) + end +end diff --git a/lib/puppet/indirector/instrumentation_listener/rest.rb b/lib/puppet/indirector/instrumentation_listener/rest.rb new file mode 100644 index 000000000..0bc8122ea --- /dev/null +++ b/lib/puppet/indirector/instrumentation_listener/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/instrumentation_listener' +require 'puppet/indirector/rest' + +class Puppet::Indirector::InstrumentationListener::Rest < Puppet::Indirector::REST +end diff --git a/lib/puppet/util/instrumentation.rb b/lib/puppet/util/instrumentation.rb index f687bfac7..33f23cb47 100644 --- a/lib/puppet/util/instrumentation.rb +++ b/lib/puppet/util/instrumentation.rb @@ -1,170 +1,171 @@ 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 which is also used in Puppet::Indirector::Indirection # which is used to setup indirections... 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". # Note: this method uses ruby yield directive to call the instrumented code. # It is usually way slower than calling start and stop directly around the instrumented code. # For high traffic code path, it is thus advisable to not use this method. 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) each_listener(label) do |k,l| l.notify(label, event, data) end end def self.listeners @listeners.values end def self.each_listener(label) synchronize { @listeners_of[label] ||= @listeners.select do |k,l| l.listen_to?(label) end }.each do |l| yield l 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 { raise "Listener #{listener.name} is already subscribed" if @listeners.include?(listener.name) Puppet.debug "registering instrumentation listener #{listener.name}" @listeners[listener.name] = Listener.new(listener, label_pattern, event) listener.subscribed if listener.respond_to?(:subscribed) rehash } end def self.unsubscribe(listener) synchronize { Puppet.warning("#{listener.name} hasn't been registered but asked to be unregistered") unless @listeners.include?(listener.name) Puppet.info "unregistering instrumentation listener #{listener.name}" @listeners.delete(listener.name) listener.unsubscribed if listener.respond_to?(:unsubscribed) rehash } end def self.init synchronize { @listeners ||= {} @listeners_of ||= {} instance_loader(:listener).loadall } 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 # should be called only under the guard # self.synchronize def self.rehash @listeners_of = {} end def self.next_id synchronize { @id = (@id || 0) + 1 } end end diff --git a/lib/puppet/util/instrumentation/data.rb b/lib/puppet/util/instrumentation/data.rb new file mode 100644 index 000000000..9157f58fc --- /dev/null +++ b/lib/puppet/util/instrumentation/data.rb @@ -0,0 +1,34 @@ +require 'puppet/indirector' +require 'puppet/util/instrumentation' + +# This is just a transport class to be used through the instrumentation_data +# indirection. All the data resides in the real underlying listeners which this +# class delegates to. +class Puppet::Util::Instrumentation::Data + extend Puppet::Indirector + + indirects :instrumentation_data, :terminus_class => :local + + attr_reader :listener + + def initialize(listener_name) + @listener = Puppet::Util::Instrumentation[listener_name] + raise "Listener #{listener_name} wasn't registered" unless @listener + end + + def name + @listener.name + end + + def to_pson(*args) + result = { + 'document_type' => "Puppet::Util::Instrumentation::Data", + 'data' => { :name => name }.merge(@listener.respond_to?(:data) ? @listener.data : {}) + } + result.to_pson(*args) + end + + def self.from_pson(data) + data + end +end diff --git a/lib/puppet/util/instrumentation/listener.rb b/lib/puppet/util/instrumentation/listener.rb index 615f38f0c..42ec0c0e9 100644 --- a/lib/puppet/util/instrumentation/listener.rb +++ b/lib/puppet/util/instrumentation/listener.rb @@ -1,55 +1,60 @@ require 'puppet/indirector' require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/data' class Puppet::Util::Instrumentation::Listener include Puppet::Util + include Puppet::Util::Warnings + 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 warnonce("Error during instrumentation notification: #{e}") end def listen_to?(label) enabled? and (!@pattern || @pattern === label.to_s) end def enabled? !!@enabled end def name @listener.name.to_s 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/indirector/instrumentation_data/local_spec.rb b/spec/unit/indirector/instrumentation_data/local_spec.rb new file mode 100644 index 000000000..45ca1e07e --- /dev/null +++ b/spec/unit/indirector/instrumentation_data/local_spec.rb @@ -0,0 +1,52 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/listener' +require 'puppet/indirector/instrumentation_data/local' + +describe Puppet::Indirector::InstrumentationData::Local do + it "should be a subclass of the Code terminus" do + Puppet::Indirector::InstrumentationData::Local.superclass.should equal(Puppet::Indirector::Code) + end + + it "should be registered with the configuration store indirection" do + indirection = Puppet::Indirector::Indirection.instance(:instrumentation_data) + Puppet::Indirector::InstrumentationData::Local.indirection.should equal(indirection) + end + + it "should have its name set to :local" do + Puppet::Indirector::InstrumentationData::Local.name.should == :local + end +end + +describe Puppet::Indirector::InstrumentationData::Local do + before :each do + Puppet::Util::Instrumentation.stubs(:listener) + @data = Puppet::Indirector::InstrumentationData::Local.new + @name = "me" + @request = stub 'request', :key => @name + end + + describe "when finding instrumentation data" do + it "should return a Instrumentation Data instance matching the key" do + end + end + + describe "when searching listeners" do + it "should raise an error" do + lambda { @data.search(@request) }.should raise_error(Puppet::DevError) + end + end + + describe "when saving listeners" do + it "should raise an error" do + lambda { @data.save(@request) }.should raise_error(Puppet::DevError) + end + end + + describe "when destroying listeners" do + it "should raise an error" do + lambda { @data.destroy(@reques) }.should raise_error(Puppet::DevError) + end + end +end diff --git a/spec/unit/indirector/instrumentation_data/rest_spec.rb b/spec/unit/indirector/instrumentation_data/rest_spec.rb new file mode 100644 index 000000000..762667ea7 --- /dev/null +++ b/spec/unit/indirector/instrumentation_data/rest_spec.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/data' +require 'puppet/indirector/instrumentation_data/rest' + +describe Puppet::Indirector::InstrumentationData::Rest do + it "should be a subclass of Puppet::Indirector::REST" do + Puppet::Indirector::InstrumentationData::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/indirector/instrumentation_listener/local_spec.rb b/spec/unit/indirector/instrumentation_listener/local_spec.rb new file mode 100644 index 000000000..b251736b4 --- /dev/null +++ b/spec/unit/indirector/instrumentation_listener/local_spec.rb @@ -0,0 +1,65 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/listener' +require 'puppet/indirector/instrumentation_listener/local' + +describe Puppet::Indirector::InstrumentationListener::Local do + it "should be a subclass of the Code terminus" do + Puppet::Indirector::InstrumentationListener::Local.superclass.should equal(Puppet::Indirector::Code) + end + + it "should be registered with the configuration store indirection" do + indirection = Puppet::Indirector::Indirection.instance(:instrumentation_listener) + Puppet::Indirector::InstrumentationListener::Local.indirection.should equal(indirection) + end + + it "should have its name set to :local" do + Puppet::Indirector::InstrumentationListener::Local.name.should == :local + end +end + +describe Puppet::Indirector::InstrumentationListener::Local do + before :each do + Puppet::Util::Instrumentation.stubs(:listener) + @listener = Puppet::Indirector::InstrumentationListener::Local.new + @name = "me" + @request = stub 'request', :key => @name + end + + describe "when finding listeners" do + it "should return a Instrumentation Listener instance matching the key" do + Puppet::Util::Instrumentation.expects(:[]).with("me").returns(:instance) + @listener.find(@request).should == :instance + end + end + + describe "when searching listeners" do + it "should return a list of all loaded Instrumentation Listenesrs irregardless of the given key" do + Puppet::Util::Instrumentation.expects(:listeners).returns([:instance1, :instance2]) + @listener.search(@request).should == [:instance1, :instance2] + end + end + + describe "when saving listeners" do + it "should set the new listener to the global listener list" do + newlistener = stub 'listener', :name => @name + @request.stubs(:instance).returns(newlistener) + Puppet::Util::Instrumentation.expects(:[]=).with("me", newlistener) + @listener.save(@request) + end + end + + describe "when destroying listeners" do + it "should raise an error if listener wasn't subscribed" do + Puppet::Util::Instrumentation.expects(:[]).with("me").returns(nil) + lambda { @listener.destroy(@request) }.should raise_error + end + + it "should unsubscribe the listener" do + Puppet::Util::Instrumentation.expects(:[]).with("me").returns(:instancce) + Puppet::Util::Instrumentation.expects(:unsubscribe).with(:instancce) + @listener.destroy(@request) + end + end +end diff --git a/spec/unit/indirector/instrumentation_listener/rest_spec.rb b/spec/unit/indirector/instrumentation_listener/rest_spec.rb new file mode 100644 index 000000000..6355a1c53 --- /dev/null +++ b/spec/unit/indirector/instrumentation_listener/rest_spec.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/listener' +require 'puppet/indirector/instrumentation_listener/rest' + +describe Puppet::Indirector::InstrumentationListener::Rest do + it "should be a subclass of Puppet::Indirector::REST" do + Puppet::Indirector::InstrumentationListener::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/util/instrumentation/data_spec.rb b/spec/unit/util/instrumentation/data_spec.rb new file mode 100755 index 000000000..c2465f622 --- /dev/null +++ b/spec/unit/util/instrumentation/data_spec.rb @@ -0,0 +1,44 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +require 'matchers/json' +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/data' + +describe Puppet::Util::Instrumentation::Data do + Puppet::Util::Instrumentation::Data + + before(:each) do + @listener = stub 'listener', :name => "name" + Puppet::Util::Instrumentation.stubs(:[]).with("name").returns(@listener) + end + + it "should indirect instrumentation_data" do + Puppet::Util::Instrumentation::Data.indirection.name.should == :instrumentation_data + end + + it "should lookup the corresponding listener" do + Puppet::Util::Instrumentation.expects(:[]).with("name").returns(@listener) + Puppet::Util::Instrumentation::Data.new("name") + end + + it "should error if the listener can not be found" do + Puppet::Util::Instrumentation.expects(:[]).with("name").returns(nil) + expect { Puppet::Util::Instrumentation::Data.new("name") }.to raise_error + end + + it "should return pson data" do + data = Puppet::Util::Instrumentation::Data.new("name") + @listener.stubs(:data).returns({ :this_is_data => "here also" }) + data.should set_json_attribute('name').to("name") + data.should set_json_attribute('this_is_data').to("here also") + end + + it "should not error if the underlying listener doesn't have data" do + lambda { Puppet::Util::Instrumentation::Data.new("name").to_pson }.should_not raise_error + end + + it "should return a hash containing data when unserializing from pson" do + Puppet::Util::Instrumentation::Data.from_pson({:name => "name"}).should == {:name => "name"} + end +end \ No newline at end of file diff --git a/spec/unit/util/instrumentation/listener_spec.rb b/spec/unit/util/instrumentation/listener_spec.rb index f2c218736..bc49d265c 100755 --- a/spec/unit/util/instrumentation/listener_spec.rb +++ b/spec/unit/util/instrumentation/listener_spec.rb @@ -1,96 +1,100 @@ #!/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 not listen to non-matching string pattern" do listener = Listener.new(@delegate, "specific") listener.enabled = true listener.should_not be_listen_to(:unspecific) 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 not listen to non matching regex pattern" do listener = Listener.new(@delegate, /^match.*/) listener.enabled = true listener.should_not be_listen_to(:not_matching) 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