├── Rakefile ├── examples └── jruby-instrument-printstream.rb ├── bin └── minstrel ├── minstrel.gemspec ├── README.textile └── lib └── minstrel.rb /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => [:package] 2 | 3 | task :test do 4 | system("cd test; ruby alltests.rb") 5 | end 6 | 7 | task :package => [:test, :package_real] do 8 | end 9 | 10 | task :package_real do 11 | system("gem build minstrel.gemspec") 12 | end 13 | 14 | task :publish do 15 | latest_gem = %x{ls -t minstrel*.gem}.split("\n").first 16 | system("gem push #{latest_gem}") 17 | end 18 | -------------------------------------------------------------------------------- /examples/jruby-instrument-printstream.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "java" 3 | require "minstrel" 4 | 5 | m = Minstrel::Instrument.new 6 | 7 | # Wrap java.io.PrintStream 8 | m.wrap(java.io.PrintStream) do |point, klass, method, *args| 9 | puts "#{point} #{klass.name || klassname}##{method}(#{args.inspect})" 10 | end 11 | 12 | # Try it. 13 | java.lang.System.out.println("Testing") 14 | -------------------------------------------------------------------------------- /bin/minstrel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | require "rubygems" 4 | require "minstrel" 5 | 6 | $0 = ARGV[0] 7 | ARGV.shift 8 | begin 9 | load $0 10 | rescue LoadError => e 11 | if File.basename($0) == $0 # if the file is just a name, not a path. 12 | found = false 13 | ENV["PATH"].split(":").each do |path| 14 | file = "#{path}/#{$0}" 15 | if File.exists?(file) 16 | load file 17 | found = true 18 | end 19 | end 20 | 21 | raise e if !found 22 | else 23 | raise e 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /minstrel.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | files = [ 3 | "./lib", 4 | "./lib/minstrel.rb", 5 | "./README.textile", 6 | "./minstrel.gemspec", 7 | ] 8 | 9 | rev = Time.now.strftime("%Y%m%d%H%M%S") 10 | spec.name = "minstrel" 11 | spec.version = "0.2.#{rev}" 12 | spec.summary = "minstrel - a ruby instrumentation tool" 13 | spec.description = "Instrument class methods" 14 | spec.files = files 15 | spec.require_paths << "lib" 16 | spec.bindir = "bin" 17 | spec.executables << "minstrel" 18 | 19 | spec.author = "Jordan Sissel" 20 | spec.email = "jls@semicomplete.com" 21 | spec.homepage = "https://github.com/jordansissel/ruby-minstrel" 22 | end 23 | 24 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. Ruby Minstrel 2 | 3 | Minstrel allows you to wrap every method call for a given class so you can more 4 | easily observe how a class and its methods are being used. 5 | 6 | h2. Get it 7 | 8 | * gem install minstrel 9 | * or download versions here: http://rubygems.org/gems/minstrel 10 | * or github: https://github.com/jordansissel/ruby-minstrel 11 | 12 | h2. Why? 13 | 14 | Fun. Also, sometimes ruby-prof and similar tools are overkill when I am trying 15 | to debug or dig into how a piece of code works. 16 | 17 | It's a lot like strace/tcpdump/dtrace for ruby, or aims to be, anyway. 18 | 19 | I wanted a tool that was useful for my own code as well as for helping 20 | understand and debug other code (puppet, mcollective, rails, activerecord, 21 | sinatra, etc...). 22 | 23 | h2. Examples 24 | 25 | h3. From the commandline 26 | 27 | You can use minstrel to wrap classes with a default 'print' wrapper that simply 28 | prints what is called. For example: 29 | 30 |
31 | % RUBY_INSTRUMENT=String ruby -rminstrel -e 'puts "hello world".capitalize.reverse' 32 | enter String#capitalize([]) 33 | exit String#capitalize([]) 34 | enter String#reverse([]) 35 | exit String#reverse([]) 36 | dlrow olleH 37 |38 | 39 | h3. The 'minstrel' tool 40 | 41 | Since the following doesn't work as expected in ruby 1.8 (or maybe all rubies): 42 | ruby -rrubygems -rminstrel ..., I provide 'minstrel' as a way to run ruby 43 | programs with minstrel preloaded. 44 | 45 |
46 | % cat test.rb 47 | #!/usr/bin/env ruby 48 | puts "hello world".capitalize.reverse 49 | 50 | % RUBY_INSTRUMENT=String minstrel test.rb 51 | enter String#capitalize([]) 52 | exit String#capitalize([]) 53 | enter String#reverse([]) 54 | exit String#reverse([]) 55 | dlrow olleH 56 |57 | 58 | h4. Example: Tracing puppet storeconfigs (aka Tracing ActiveRecord queries) 59 | 60 | ActiveRecord has a base class for most things query-related. Let's trace that: 61 | 62 |
63 | % sudo env RUBY_INSTRUMENT=ActiveRecord::ConnectionAdapters::DatabaseStatements minstrel puppet ... 64 | enter ActiveRecord::ConnectionAdapters::DatabaseStatements#select_all(["SELECT DISTINCT 65 | `hosts`.id FROM `hosts` LEFT OUTER JOIN `fact_values` ON `fact_values`.`host_id` = `h 66 | osts`.`id` LEFT OUTER JOIN `fact_names` ON `fact_names`.`id` = `fact_values`.`fact_name_id` 67 | ... 68 | enter ActiveRecord::ConnectionAdapters::DatabaseStatements#select_all(["SELECT `hosts`.`id` AS t0_r0, `hosts`.`name` AS t0_r1, `hosts`.`ip` AS t0_r2, `hosts`.`environment ... 69 |70 | 71 | So easy :) 72 | 73 | 74 | h3. From ruby 75 | 76 | Boilerplate: 77 | 78 |
79 | require "minstrel" 80 | 81 | instrument = Minstrel::Instrument.new() 82 | instrument.wrap(String) do |point, klass, method, *args| 83 | # * point is either :enter or :exit depending if this function is about to be 84 | # called or has finished being called. 85 | # * klass is the class object (String, etc) 86 | # * method is the method (a Symbol) 87 | # * *args is the arguments passed 88 | end 89 |90 | 91 | Example: 92 | 93 |
94 | require "minstrel"
95 |
96 | class Foo
97 | def bar(one, &block)
98 | yield one
99 | end
100 |
101 | def baz
102 | puts "Baz!"
103 | end
104 | end
105 |
106 | instrument = Minstrel::Instrument.new
107 | instrument.wrap(Foo) do |point, klass, method, *args|
108 | puts "#{point} #{klass.name}##{method}(#{args.inspect})"
109 | end
110 |
111 | foo = Foo.new
112 | foo.bar(123) { |arg| puts arg }
113 | foo.baz
114 |
115 |
116 | Output:
117 |
118 | 119 | enter Foo#bar([123]) 120 | 123 121 | exit Foo#bar([123]) 122 | enter Foo#baz([]) 123 | Baz! 124 | exit Foo#baz([]) 125 |126 | 127 | h3. From ruby (deferred loading) 128 | 129 | Sometimes you don't know when a class is going to be defined. To solve this, 130 | you must use Minstrel::Instrument#wrap_classname. For example: 131 | 132 |
133 | >> require "minstrel"
134 | >> Minstrel::Instrument.new.wrap_classname("TCPSocket")
135 | >> require "socket"
136 | Wrap of TCPSocket successful
137 |
138 |
139 | Minstrel will wrap 'require' and check for classes you want wrapped at each
140 | require until it finds all the classes you asked to be wrapped.
141 |
142 | h2. Caveats
143 |
144 | Metaprogramming will not be often caught, necessarily, by minstrel, because they don't
145 | show usuall up as methods. However, the things invoking metaprogramming are
146 | usually methods so in most cases you'll get lucky enough to see what's going
147 | on.
148 |
149 | Some cases of metaprogramming (dynamic method generation, DSLs, etc) can be
150 | caught if you call Minstrel::Instrument#wrap() late enough in the lifetime of
151 | the program that the dynamic methods have been created.
152 |
153 | h2. Bugs?
154 |
155 | If you find bugs, have feature suggestions, etc, feel free to open bugs here on
156 | github (https://github.com/jordansissel/ruby-minstrel/issues). I also read email:
157 | jls@semicomplete.com
158 |
--------------------------------------------------------------------------------
/lib/minstrel.rb:
--------------------------------------------------------------------------------
1 | # Wrap method calls for a class of your choosing.
2 | # Example:
3 | # instrument = Minstrel::Instrument.new()
4 | # instrument.wrap(String) do |point, klass, method, *args|
5 | # ...
6 | # end
7 | #
8 | # * point is either :enter or :exit depending if this function is about to be
9 | # called or has finished being called.
10 | # * klass is the class object (String, etc)
11 | # * method is the method (a Symbol)
12 | # * *args is the arguments passed
13 | #
14 | # You can also wrap from the command-line
15 | #
16 | # RUBY_INSTRUMENT=comma_separated_classnames ruby -rminstrel ./your/program.rb
17 | #
18 |
19 | require "set"
20 |
21 | module Minstrel; class Instrument
22 | attr_accessor :counter
23 |
24 | class << self
25 | @@deferred_wraps = {}
26 | @@deferred_method_wraps = {}
27 | @@wrapped = Set.new
28 | end
29 |
30 | # Put methods we must not be wrapping here.
31 | DONOTWRAP = {
32 | "Minstrel::Instrument" => Minstrel::Instrument.instance_methods.collect { |m| m.to_sym },
33 | "Object" => [ :to_sym, :respond_to?, :send, :java_send, :method, :java_method,
34 | :ancestors, :inspect, :to_s, :instance_eval, :instance_exec,
35 | :class_eval, :class_exec, :module_eval, :module_exec],
36 | }
37 |
38 | # Wrap a class's instance methods with your block.
39 | # The block will be called with 4 arguments, and called
40 | # before and after the original method.
41 | # Arguments:
42 | # * point - the point (symbol, :entry or :exit) of call,
43 | # * this - the object instance in scope (use 'this.class' for the class)
44 | # * method - the method (symbol) being called
45 | # * *args - the arguments (array) passed to this method.
46 | def wrap(klass, method_to_wrap=nil, &block)
47 | return true if @@wrapped.include?(klass)
48 | instrumenter = self # save 'self' for scoping below
49 | @@wrapped << klass
50 |
51 | ancestors = klass.ancestors.collect {|k| k.to_s }
52 | if ancestors.include?("Exception")
53 | return true
54 | end
55 | puts "Wrapping #{klass.class} #{klass}" if $DEBUG
56 |
57 | # Wrap class instance methods (like File#read)
58 | klass.instance_methods.each do |method|
59 | next if !method_to_wrap.nil? and method != method_to_wrap
60 |
61 | method = method.to_sym
62 |
63 | # If we shouldn't wrap a certain class method, skip it.
64 | skip = false
65 | (ancestors & DONOTWRAP.keys).each do |key|
66 | if DONOTWRAP[key].include?(method)
67 | skip = true
68 | break
69 | end
70 | end
71 | if skip
72 | #puts "Skipping #{klass}##{method} (do not wrap)"
73 | next
74 | end
75 |
76 | klass.class_eval do
77 | orig_method = "#{method}_original(wrapped)".to_sym
78 | orig_method_proc = klass.instance_method(method)
79 | alias_method orig_method, method
80 | #block.call(:wrap, klass, method)
81 | puts "Wrapping #{klass.name}##{method} (method)" if $DEBUG
82 | define_method(method) do |*args, &argblock|
83 | exception = false
84 | block.call(:enter, self, method, *args)
85 | begin
86 | # TODO(sissel): Not sure which is better:
87 | # * UnboundMethod#bind(self).call(...)
88 | # * self.method(orig_method).call(...)
89 | val = orig_method_proc.bind(self).call(*args, &argblock)
90 | #m = self.method(orig_method)
91 | #val = m.call(*args, &argblock)
92 | rescue => e
93 | exception = e
94 | end
95 | if exception
96 | # TODO(sissel): Include the exception
97 | block.call(:exit_exception, self, method, *args)
98 | raise e if exception
99 | else
100 | # TODO(sissel): Include the return value
101 | block.call(:exit, self, method, *args)
102 | end
103 | return val
104 | end # define_method(method)
105 | end # klass.class_eval
106 | end # klass.instance_methods.each
107 |
108 | # Wrap class methods (like File.open)
109 | klass.methods.each do |method|
110 | next if !method_to_wrap.nil? and method != method_to_wrap
111 | method = method.to_sym
112 | # If we shouldn't wrap a certain class method, skip it.
113 | skip = false
114 | ancestors = klass.ancestors.collect {|k| k.to_s}
115 | (ancestors & DONOTWRAP.keys).each do |key|
116 | if DONOTWRAP[key].include?(method)
117 | skip = true
118 | #break
119 | end
120 | end
121 |
122 | # Doubly-ensure certain methods are not wrapped.
123 | # Some classes like "Timeout" do not have ancestors.
124 | if DONOTWRAP["Object"].include?(method)
125 | #puts "!! Skipping #{klass}##{method} (do not wrap)"
126 | skip = true
127 | end
128 |
129 | if skip
130 | puts "Skipping #{klass}##{method} (do not wrap, not safe)" if $DEBUG
131 | next
132 | end
133 |
134 | klass.instance_eval do
135 | orig_method = "#{method}_original(classwrapped)".to_sym
136 | (class << self; self; end).instance_eval do
137 | begin
138 | alias_method orig_method, method.to_sym
139 | rescue NameError => e
140 | # No such method, strange but true.
141 | orig_method = self.method(method.to_sym)
142 | end
143 | method = method.to_sym
144 | #puts "Wrapping #{klass.name}.#{method} (classmethod)"
145 | define_method(method) do |*args, &argblock|
146 | block.call(:class_enter, self, method, *args)
147 | exception = false
148 | begin
149 | if orig_method.is_a?(Symbol)
150 | val = send(orig_method, *args, &argblock)
151 | else
152 | val = orig_method.call(*args, &argblock)
153 | end
154 | rescue => e
155 | exception = e
156 | end
157 | if exception
158 | block.call(:class_exit_exception, self, method, *args)
159 | raise e if exception
160 | else
161 | block.call(:class_exit, self, method, *args)
162 | end
163 | return val
164 | end
165 | end
166 | #block.call(:class_wrap, self, method, self.method(method))
167 | end # klass.instance_eval
168 | end # klass.instance_methods.each
169 |
170 | return true
171 | end # def wrap
172 |
173 | def wrap_classname(klassname, &block)
174 | begin
175 | klass = eval(klassname)
176 | wrap(klass, &block)
177 | return true
178 | rescue NameError => e
179 | @@deferred_wraps[klassname] = block
180 | end
181 | return false
182 | end
183 |
184 | def wrap_method(fullname, &block)
185 | puts "Want to wrap #{fullname}" if $DEBUG
186 | begin
187 | klassname, method = fullname.split(/[#.]/, 2)
188 | klass = eval(klassname)
189 | wrap(klass, method, &block)
190 | return true
191 | rescue NameError => e
192 | @@deferred_method_wraps[fullname] = block
193 | return false
194 | end
195 | end # def wrap_method
196 |
197 | def wrap_all(&block)
198 | @@deferred_wraps[:all] = block
199 | ObjectSpace.each_object do |obj|
200 | next unless obj.is_a?(Class)
201 | wrap(obj, &block)
202 | end
203 | end
204 |
205 | def self.wrap_require
206 | Kernel.class_eval do
207 | alias_method :old_require, :require
208 | def require(*args)
209 | return Minstrel::Instrument::instrumented_loader(:require, *args)
210 | end
211 | end
212 | end
213 |
214 | def self.wrap_load
215 | Kernel.class_eval do
216 | alias_method :old_load, :load
217 | def load(*args)
218 | return Minstrel::Instrument::instrumented_loader(:load, *args)
219 | end
220 | end
221 | end
222 |
223 | def self.instrumented_loader(method, *args)
224 | ret = self.send(:"old_#{method}", *args)
225 | if @@deferred_wraps.include?(:all)
226 | # try to wrap anything new that is not wrapped
227 | wrap_all(@@deferred_wraps[:all])
228 | else
229 | # look for deferred class wraps
230 | klasses = @@deferred_wraps.keys
231 | klasses.each do |klassname|
232 | if @@deferred_wraps.include?("ALL")
233 | all = true
234 | end
235 | block = @@deferred_wraps[klassname]
236 | instrument = Minstrel::Instrument.new
237 | if instrument.wrap_classname(klassname, &block)
238 | $stderr.puts "Wrap of #{klassname} successful"
239 | @@deferred_wraps.delete(klassname) if !all
240 | end
241 | end
242 |
243 | klassmethods = @@deferred_method_wraps.keys
244 | klassmethods.each do |fullname|
245 | block = @@deferred_method_wraps[fullname]
246 | instrument = Minstrel::Instrument.new
247 | if instrument.wrap_method(fullname, &block)
248 | $stderr.puts "Wrap of #{fullname} successful"
249 | @@deferred_method_wraps.delete(fullname)
250 | end
251 | end
252 | end
253 | return ret
254 | end
255 | end; end # class Minstrel::Instrument
256 |
257 | Minstrel::Instrument.wrap_require
258 | Minstrel::Instrument.wrap_load
259 |
260 | # Provide a way to instrument a class using the command line:
261 | # RUBY_INSTRUMENT=String ruby -rminstrel ./your/program
262 | if ENV["RUBY_INSTRUMENT"]
263 | klasses = ENV["RUBY_INSTRUMENT"].split(",")
264 |
265 | callback = proc do |point, this, method, *args|
266 | puts "#{point} #{this.class.to_s}##{method}(#{args.inspect}) (thread=#{Thread.current}, self=#{this.inspect})"
267 | end
268 | instrument = Minstrel::Instrument.new
269 | if klasses.include?(":all:")
270 | instrument.wrap_all(&callback)
271 | else
272 | klasses.each do |klassname|
273 | if klassname =~ /[#.]/ # Someone's asking for a specific method to wrap
274 | # This will wrap one method as indicated by: ClassName#method
275 | # TODO(sissel): Maybe also allow ModuleName::method
276 | instrument.wrap_method(klassname, &callback)
277 | else
278 | instrument.wrap_classname(klassname, &callback)
279 | end
280 | end # klasses.each
281 | end
282 | end # if ENV["RUBY_INSTRUMENT"]
283 |
--------------------------------------------------------------------------------