Module: Sixty
- Defined in:
- lib/sixty.rb,
lib/sixty/sql.rb,
lib/sixty/plans.rb,
lib/sixty/shape.rb,
lib/sixty/stack.rb,
lib/sixty/config.rb,
lib/sixty/sketch.rb,
lib/sixty/tracer.rb,
lib/sixty/railtie.rb,
lib/sixty/version.rb,
lib/sixty/exporter.rb,
lib/sixty/aggregator.rb,
lib/sixty/instrumented.rb,
lib/sixty/instrument/pg.rb,
lib/sixty/instrument/rack.rb,
lib/sixty/instrument/mongo.rb,
lib/sixty/instrument/mysql.rb,
lib/sixty/instrument/active_record.rb,
lib/sixty/instrument/action_controller.rb
Overview
sixty — zero-configuration performance drift detection, for Ruby.
require 'sixty'
Sixty.init # reads SIXTY_API_KEY, SIXTY_SERVICE, SIXTY_RELEASE from ENV
In a Rails application the railtie calls this for you and installs the Rack middleware, the ActiveRecord subscriber and the controller instrumentation, so the only line an application needs is the gem in its Gemfile.
What it reports is deliberately not latency alone. It is shape: how many rows a query returned, how many queries a method issued, how much of a request was spent in your own code rather than below it. Those are the numbers that barely move on a warm development database and take production down a week later.
Defined Under Namespace
Modules: Instrument, Instrumented, Plans, Shape, Sql, Stack, Tracer Classes: Aggregator, Config, Exporter, Railtie, Sketch
Constant Summary collapse
- VERSION =
'0.1.0'
Class Attribute Summary collapse
-
.aggregator ⇒ Object
readonly
Returns the value of attribute aggregator.
-
.config ⇒ Object
readonly
Returns the value of attribute config.
-
.exporter ⇒ Object
readonly
Returns the value of attribute exporter.
Class Method Summary collapse
-
.annotate(key, value) ⇒ Object
Record a value observed inside the current operation — e.g.
-
.before_flush(&block) ⇒ Object
Work that should happen on the agent's thread, just before a flush.
- .enabled? ⇒ Boolean
-
.flush(timeout: Exporter::TIMEOUT_SECONDS) ⇒ Object
Flush now, from whatever thread asks.
- .init(options = {}) ⇒ Object
-
.instrument(klass, *names) ⇒ Object
Instrument methods on a class you do not own — a gem's client, a model generated elsewhere, anything you cannot add an
includeto. -
.on_span_end(span) ⇒ Object
Called on every completed span.
-
.reset! ⇒ Object
Test seam.
-
.set_route(route) ⇒ Object
Upgrade the current request's span to a framework-supplied route pattern.
-
.trace(name, kind: Tracer::KIND_FUNCTION, attrs: nil) ⇒ Object
Measure a block as one operation.
Class Attribute Details
.aggregator ⇒ Object (readonly)
Returns the value of attribute aggregator.
31 32 33 |
# File 'lib/sixty.rb', line 31 def aggregator @aggregator end |
.config ⇒ Object (readonly)
Returns the value of attribute config.
31 32 33 |
# File 'lib/sixty.rb', line 31 def config @config end |
.exporter ⇒ Object (readonly)
Returns the value of attribute exporter.
31 32 33 |
# File 'lib/sixty.rb', line 31 def exporter @exporter end |
Class Method Details
.annotate(key, value) ⇒ Object
Record a value observed inside the current operation — e.g. the size of a result the caller cares about. Attaches to the active span.
114 115 116 117 |
# File 'lib/sixty.rb', line 114 def annotate(key, value) span = Tracer.current span.attrs[key] = value if span end |
.before_flush(&block) ⇒ Object
Work that should happen on the agent's thread, just before a flush.
The ActiveRecord instrumentation registers the EXPLAIN pass here: it needs a database connection and must never run on the request path, and this is the one thread in the process that satisfies both.
159 160 161 |
# File 'lib/sixty.rb', line 159 def before_flush(&block) (@flush_hooks ||= []) << block end |
.enabled? ⇒ Boolean
94 95 96 |
# File 'lib/sixty.rb', line 94 def enabled? @enabled == true end |
.flush(timeout: Exporter::TIMEOUT_SECONDS) ⇒ Object
Flush now, from whatever thread asks. Used by the interval thread, by at_exit, and by tests that cannot wait fifteen seconds.
145 146 147 148 149 150 151 152 |
# File 'lib/sixty.rb', line 145 def flush(timeout: Exporter::TIMEOUT_SECONDS) return unless enabled? run_flush_hooks exporter.flush(aggregator.drain, timeout: timeout) rescue StandardError => e config&.on_warn&.call("sixty: flush failed: #{e.}") end |
.init(options = {}) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/sixty.rb', line 33 def init( = {}) return @state if @state config = Config.new() @config = config unless config.active? config.on_warn.call( 'sixty: no API key found (set SIXTY_API_KEY or pass api_key:). Agent is inactive.' ) return nil end Stack.root = rails_root || Dir.pwd @aggregator = Aggregator.new(on_warn: config.on_warn) @exporter = Exporter.new( endpoint: config.endpoint, api_key: config.api_key, service: config.service, environment: config.environment, release: config.release, repo_url: config.repo_url, on_warn: config.on_warn ) Tracer.sink = method(:on_span_end) @enabled = true @flush_hooks = [] start_flusher install_database_instrumentation(config) # Retried on every flush, because a driver is a constant that may not # exist yet. `Sixty.init` in a Sinatra app can easily run before # `require 'mysql2'`, and an agent that decided once at boot would report # no queries at all for the rest of the process — silently, which is the # failure this project refuses. Every installer is idempotent, so the # retry costs a few `defined?` checks a minute. before_flush { install_database_instrumentation(config) } # Rack and ActionController install themselves through the railtie when # Rails is present. A plain Rack or Sinatra app calls # Sixty::Instrument::Rack directly, which is why neither is here. # # The last window is worth one short attempt and no more. A process being # asked to stop is often a deploy waiting on it, and an agent that can add # ten seconds to every shutdown because a collector is unreachable is an # agent that will be removed — correctly. at_exit { flush(timeout: 2) } @state = { config: config, aggregator: @aggregator, exporter: @exporter } if config.debug config.on_warn.call( "sixty: active — service=#{config.service} env=#{config.environment} " \ "release=#{config.release.empty? ? '(none)' : config.release} " \ "endpoint=#{config.endpoint}" ) end @state end |
.instrument(klass, *names) ⇒ Object
Instrument methods on a class you do not own — a gem's client, a model
generated elsewhere, anything you cannot add an include to.
Sixty.instrument(Stripe::Charge, :create)
Only the named methods are wrapped, and no method_added hook is
installed: a class you do not own may define methods long after this call
— lazily, or through a gem's own metaprogramming — and silently
instrumenting those is not something a one-line call should decide.
200 201 202 |
# File 'lib/sixty/instrumented.rb', line 200 def instrument(klass, *names) names.each { |name| Instrumented.wrap(klass, name) } end |
.on_span_end(span) ⇒ Object
Called on every completed span. Everything downstream of here is the agent's own work, so it is wrapped: an agent bug must never surface as an application error.
135 136 137 138 139 140 141 |
# File 'lib/sixty.rb', line 135 def on_span_end(span) ensure_flusher aggregator.record(span) keep_exemplar(span) rescue StandardError => e config.on_warn.call("sixty: internal error recording span: #{e.}") end |
.reset! ⇒ Object
Test seam. Forgets everything and stops the flush thread.
164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/sixty.rb', line 164 def reset! @flusher&.kill @flusher = nil @state = nil @enabled = false @flush_hooks = [] Tracer.sink = nil Tracer.current = nil Stack.reset! Plans.reset! end |
.set_route(route) ⇒ Object
Upgrade the current request's span to a framework-supplied route pattern. A real route always beats the path heuristic: without it every /users/42 is its own operation.
122 123 124 125 126 127 128 129 130 |
# File 'lib/sixty.rb', line 122 def set_route(route) span = Tracer.current return unless span && route.is_a?(String) && !route.empty? root = span.root return unless root && root.kind == Tracer::KIND_HTTP root.name = "#{root.attrs[:method]} #{route}" end |
.trace(name, kind: Tracer::KIND_FUNCTION, attrs: nil) ⇒ Object
Measure a block as one operation.
The escape hatch for code the automatic instrumentation cannot see, and
the building block Sixty::Instrumented uses. name is an identity that
will be compared across releases, so it must not contain anything that
varies per call — an id in a name mints an operation per id and blows the
cardinality cap.
105 106 107 108 109 110 |
# File 'lib/sixty.rb', line 105 def trace(name, kind: Tracer::KIND_FUNCTION, attrs: nil) return yield unless enabled? span = Tracer.start_span(kind: kind, name: name, attrs: attrs) Tracer.in_span(span) { yield } end |