Module: Sixty::Instrumented
- Defined in:
- lib/sixty/instrumented.rb
Overview
Your own code, measured.
class OrdersQuery
include Sixty::Instrumented
def for_user(id) = Order.where(user_id: id).limit(30).to_a
def enrich(orders) = ...
end
Every public instance method defined after the include becomes an operation: its own latency, its self time, and — the number that matters — how many queries and how many rows it is responsible for, including everything it calls.
── Why this is opt-in ────────────────────────────────────────────────────
The Node agent wraps functions with a build transform, which can afford to
instrument everything because it happens once at build time. Ruby has no
build step: the equivalent would be walking ObjectSpace at boot and
prepending a module to every class in app/, which is both slow and a
decision nobody asked us to make on their behalf — including on classes
whose methods are called in a tight loop, where a span per call is real
overhead for no signal.
So this is a line the application writes on the classes worth measuring: services, query objects, jobs. Controllers and queries are covered automatically by the railtie, which is enough for the feed to be useful before anybody adds a single include.
── What it costs when the agent is off ───────────────────────────────────
One method call. Sixty.trace checks a boolean and yields, so an
uninitialized agent — every test suite, every rake task, every developer's
console — adds a frame and nothing else.
Defined Under Namespace
Modules: ClassMethods
Class Method Summary collapse
- .included(base) ⇒ Object
- .revisibility(klass, visibility, names) ⇒ Object
-
.unwrap(klass, names) ⇒ Object
Drop the wrapper for methods that turned out to be private.
-
.wrap(klass, name) ⇒ Object
Wrap one instance method of
klass. - .wrap_singleton(klass, name) ⇒ Object
-
.wrapper_for(klass) ⇒ Object
The wrapping lives in a module prepended to the class rather than in an alias chain:
superthen reaches the original definition, subclasses andprepended concerns keep working, and removing the include removes the instrumentation completely.
Class Method Details
.included(base) ⇒ Object
42 43 44 45 |
# File 'lib/sixty/instrumented.rb', line 42 def self.included(base) base.extend(ClassMethods) base.sixty_install_wrapper! end |
.revisibility(klass, visibility, names) ⇒ Object
148 149 150 151 152 153 154 155 |
# File 'lib/sixty/instrumented.rb', line 148 def revisibility(klass, visibility, names) wrapper = existing_wrapper(klass) return unless wrapper names.each do |name| wrapper.send(visibility, name) if wrapper.method_defined?(name) end end |
.unwrap(klass, names) ⇒ Object
Drop the wrapper for methods that turned out to be private.
139 140 141 142 143 144 145 146 |
# File 'lib/sixty/instrumented.rb', line 139 def unwrap(klass, names) wrapper = existing_wrapper(klass) return unless wrapper names.each do |name| wrapper.send(:remove_method, name) if wrapper.method_defined?(name) end end |
.wrap(klass, name) ⇒ Object
Wrap one instance method of klass.
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/sixty/instrumented.rb', line 113 def wrap(klass, name) return unless klass.public_method_defined?(name) || klass.protected_method_defined?(name) # An accessor is not an operation. `attr_reader :total` reports a span # per read of an instance variable — pure overhead, and it buries the # methods that do something in a list of the ones that do not. return if name.to_s.end_with?('=') original = klass.instance_method(name) return if original.source_location.nil? operation = "#{klass.name || 'anonymous'}##{name}" attrs = location_attrs(original) wrapper = wrapper_for(klass) return if wrapper.method_defined?(name) wrapper.send(:define_method, name) do |*args, &block| Sixty.trace(operation, attrs: attrs.dup) { super(*args, &block) } end # Keyword arguments survive the splat only if the wrapper is marked: # without this, `save(validate: false)` arrives at the original method # as a positional Hash on Ruby 3, which is a behaviour change an # observability gem has no business introducing. wrapper.send(:ruby2_keywords, name) end |
.wrap_singleton(klass, name) ⇒ Object
157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/sixty/instrumented.rb', line 157 def wrap_singleton(klass, name) return unless klass.singleton_class.method_defined?(name) original = klass.singleton_class.instance_method(name) operation = "#{klass.name || 'anonymous'}.#{name}" attrs = location_attrs(original) wrapper = Module.new do define_method(name) do |*args, &block| Sixty.trace(operation, attrs: attrs.dup) { super(*args, &block) } end ruby2_keywords name end klass.singleton_class.prepend(wrapper) end |
.wrapper_for(klass) ⇒ Object
The wrapping lives in a module prepended to the class rather than in an
alias chain: super then reaches the original definition, subclasses
and prepended concerns keep working, and removing the include removes
the instrumentation completely.
101 102 103 104 105 106 107 108 109 110 |
# File 'lib/sixty/instrumented.rb', line 101 def wrapper_for(klass) if klass.instance_variable_defined?(:@sixty_wrapper) klass.instance_variable_get(:@sixty_wrapper) else wrapper = Module.new klass.instance_variable_set(:@sixty_wrapper, wrapper) klass.prepend(wrapper) wrapper end end |