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

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