├── 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 | --------------------------------------------------------------------------------