scout_apm 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,200 @@
1
+ # The store encapsolutes the logic that (1) saves instrumented data by Metric name to memory and (2) maintains a stack (just an Array)
2
+ # of instrumented methods that are being called. It's accessed via +ScoutApm::Agent.instance.store+.
3
+ class ScoutApm::Store
4
+
5
+ # Limits the size of the metric hash to prevent a metric explosion.
6
+ MAX_SIZE = 1000
7
+
8
+ # Limit the number of slow transactions that we store metrics with to prevent writing too much data to the layaway file if there are are many processes and many slow slow_transactions.
9
+ MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS = 10
10
+
11
+ attr_accessor :metric_hash
12
+ attr_accessor :transaction_hash
13
+ attr_accessor :stack
14
+ attr_accessor :slow_transactions # array of slow transaction slow_transactions
15
+ attr_reader :slow_transaction_lock
16
+
17
+ def initialize
18
+ @metric_hash = Hash.new
19
+ # Stores aggregate metrics for the current transaction. When the transaction is finished, metrics
20
+ # are merged with the +metric_hash+.
21
+ @transaction_hash = Hash.new
22
+ @stack = Array.new
23
+ # ensure background thread doesn't manipulate transaction sample while the store is.
24
+ @slow_transaction_lock = Mutex.new
25
+ @slow_transactions = Array.new
26
+ end
27
+
28
+ # Called when the last stack item completes for the current transaction to clear
29
+ # for the next run.
30
+ def reset_transaction!
31
+ Thread::current[:scout_apm_ignore_transaction] = nil
32
+ Thread::current[:scout_apm_scope_name] = nil
33
+ @transaction_hash = Hash.new
34
+ @stack = Array.new
35
+ end
36
+
37
+ def ignore_transaction!
38
+ Thread::current[:scout_apm_ignore_transaction] = true
39
+ end
40
+
41
+ # Called at the start of Tracer#instrument:
42
+ # (1) Either finds an existing MetricStats object in the metric_hash or
43
+ # initialize a new one. An existing MetricStats object is present if this +metric_name+ has already been instrumented.
44
+ # (2) Adds a StackItem to the stack. This StackItem is returned and later used to validate the item popped off the stack
45
+ # when an instrumented code block completes.
46
+ def record(metric_name)
47
+ item = ScoutApm::StackItem.new(metric_name)
48
+ stack << item
49
+ item
50
+ end
51
+
52
+ # Options:
53
+ # * :scope - If specified, sets the sub-scope for the metric. We allow additional scope level. This is used
54
+ # * uri - the request uri
55
+ def stop_recording(sanity_check_item, options={})
56
+ item = stack.pop
57
+ stack_empty = stack.empty?
58
+ # if ignoring the transaction, the item is popped but nothing happens.
59
+ if Thread::current[:scout_apm_ignore_transaction]
60
+ return
61
+ end
62
+ # unbalanced stack check - unreproducable cases have seen this occur. when it does, sets a Thread variable
63
+ # so we ignore further recordings. +Store#reset_transaction!+ resets this.
64
+ if item != sanity_check_item
65
+ ScoutApm::Agent.instance.logger.warn "Scope [#{Thread::current[:scout_apm_scope_name]}] Popped off stack: #{item.inspect} Expected: #{sanity_check_item.inspect}. Aborting."
66
+ ignore_transaction!
67
+ return
68
+ end
69
+ duration = Time.now - item.start_time
70
+ if last=stack.last
71
+ last.children_time += duration
72
+ end
73
+ meta = ScoutApm::MetricMeta.new(item.metric_name, :desc => options[:desc])
74
+ meta.scope = nil if stack_empty
75
+
76
+ # add backtrace for slow calls ... how is exclusive time handled?
77
+ if duration > ScoutApm::SlowTransaction::BACKTRACE_THRESHOLD and !stack_empty
78
+ meta.extra = {:backtrace => ScoutApm::SlowTransaction.backtrace_parser(caller)}
79
+ end
80
+ stat = transaction_hash[meta] || ScoutApm::MetricStats.new(!stack_empty)
81
+ stat.update!(duration,duration-item.children_time)
82
+ transaction_hash[meta] = stat if store_metric?(stack_empty)
83
+
84
+ # Uses controllers as the entry point for a transaction. Otherwise, stats are ignored.
85
+ if stack_empty and meta.metric_name.match(/\AController\//)
86
+ aggs=aggregate_calls(transaction_hash.dup,meta)
87
+ store_slow(options[:uri],transaction_hash.dup.merge(aggs),meta,stat)
88
+ # deep duplicate
89
+ duplicate = aggs.dup
90
+ duplicate.each_pair do |k,v|
91
+ duplicate[k.dup] = v.dup
92
+ end
93
+ merge_metrics(duplicate.merge({meta.dup => stat.dup})) # aggregrates + controller
94
+ end
95
+ end
96
+
97
+ # TODO - Move more logic to SlowTransaction
98
+ #
99
+ # Limits the size of the transaction hash to prevent a large transactions. The final item on the stack
100
+ # is allowed to be stored regardless of hash size to wrapup the transaction sample w/the parent metric.
101
+ def store_metric?(stack_empty)
102
+ transaction_hash.size < ScoutApm::SlowTransaction::MAX_SIZE or stack_empty
103
+ end
104
+
105
+ # Returns the top-level category names used in the +metrics+ hash.
106
+ def categories(metrics)
107
+ cats = Set.new
108
+ metrics.keys.each do |meta|
109
+ next if meta.scope.nil? # ignore controller
110
+ if match=meta.metric_name.match(/\A([\w|\d]+)\//)
111
+ cats << match[1]
112
+ end
113
+ end # metrics.each
114
+ cats
115
+ end
116
+
117
+ # Takes a metric_hash of calls and generates aggregates for ActiveRecord and View calls.
118
+ def aggregate_calls(metrics,parent_meta)
119
+ categories = categories(metrics)
120
+ aggregates = {}
121
+ categories.each do |cat|
122
+ agg_meta=ScoutApm::MetricMeta.new("#{cat}/all")
123
+ agg_meta.scope = parent_meta.metric_name
124
+ agg_stats = ScoutApm::MetricStats.new
125
+ metrics.each do |meta,stats|
126
+ if meta.metric_name =~ /\A#{cat}\//
127
+ agg_stats.combine!(stats)
128
+ end
129
+ end # metrics.each
130
+ aggregates[agg_meta] = agg_stats unless agg_stats.call_count.zero?
131
+ end # categories.each
132
+ aggregates
133
+ end
134
+
135
+ # Stores slow transactions. This will be sent to the server.
136
+ def store_slow(uri,transaction_hash,parent_meta,parent_stat,options = {})
137
+ @slow_transaction_lock.synchronize do
138
+ # tree map of all slow transactions
139
+ if parent_stat.total_call_time >= 2
140
+ @slow_transactions.push(ScoutApm::SlowTransaction.new(uri,parent_meta.metric_name,parent_stat.total_call_time,transaction_hash.dup,ScoutApm::Context.current))
141
+ ScoutApm::Agent.instance.logger.debug "Slow transaction sample added. [URI: #{uri}] [Context: #{ScoutApm::Context.current.to_hash}] Array Size: #{@slow_transactions.size}" end
142
+ end
143
+ end
144
+
145
+ # Finds or creates the metric w/the given name in the metric_hash, and updates the time. Primarily used to
146
+ # record sampled metrics. For instrumented methods, #record and #stop_recording are used.
147
+ #
148
+ # Options:
149
+ # :scope => If provided, overrides the default scope.
150
+ # :exclusive_time => Sets the exclusive time for the method. If not provided, uses +call_time+.
151
+ def track!(metric_name,call_time,options = {})
152
+ meta = ScoutApm::MetricMeta.new(metric_name)
153
+ meta.scope = options[:scope] if options.has_key?(:scope)
154
+ stat = metric_hash[meta] || ScoutApm::MetricStats.new
155
+ stat.update!(call_time,options[:exclusive_time] || call_time)
156
+ metric_hash[meta] = stat
157
+ end
158
+
159
+ # Combines old and current data
160
+ def merge_data(old_data)
161
+ {:metrics => merge_metrics(old_data[:metrics]), :slow_transactions => merge_slow_transactions(old_data[:slow_transactions])}
162
+ end
163
+
164
+ # Merges old and current data, clears the current in-memory metric hash, and returns
165
+ # the merged data
166
+ def merge_data_and_clear(old_data)
167
+ merged = merge_data(old_data)
168
+ self.metric_hash = {}
169
+ # TODO - is this lock needed?
170
+ @slow_transaction_lock.synchronize do
171
+ self.slow_transactions = []
172
+ end
173
+ merged
174
+ end
175
+
176
+ def merge_metrics(old_metrics)
177
+ old_metrics.each do |old_meta,old_stats|
178
+ if stats = metric_hash[old_meta]
179
+ metric_hash[old_meta] = stats.combine!(old_stats)
180
+ elsif metric_hash.size < MAX_SIZE
181
+ metric_hash[old_meta] = old_stats
182
+ end
183
+ end
184
+ metric_hash
185
+ end
186
+
187
+ # Merges slow_transactions together, removing transaction sample metrics from slow_transactions if the > MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS
188
+ def merge_slow_transactions(old_slow_transactions)
189
+ # need transaction lock here?
190
+ self.slow_transactions += old_slow_transactions
191
+ if trim_slow_transactions = self.slow_transactions[MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS..-1]
192
+ ScoutApm::Agent.instance.logger.debug "Trimming metrics from #{trim_slow_transactions.size} slow_transactions."
193
+ i = MAX_SLOW_TRANSACTIONS_TO_STORE_METRICS
194
+ trim_slow_transactions.each do |sample|
195
+ self.slow_transactions[i] = sample.clear_metrics!
196
+ end
197
+ end
198
+ self.slow_transactions
199
+ end
200
+ end # class Store
@@ -0,0 +1,112 @@
1
+ # Contains the methods that instrument blocks of code.
2
+ #
3
+ # When a code block is wrapped inside #instrument(metric_name):
4
+ # * The #instrument method pushes a StackItem onto Store#stack
5
+ # * When a code block is finished, #instrument pops the last item off the stack and verifies it's the StackItem
6
+ # we created earlier.
7
+ # * Once verified, the metrics for the recording session are merged into the in-memory Store#metric_hash. The current scope
8
+ # is also set for the metric (if Thread::current[:scout_apm_scope_name] isn't nil).
9
+ module ScoutApm::Tracer
10
+ def self.included(klass)
11
+ klass.extend ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ # Use to trace a method call, possibly reporting slow transaction traces to Scout.
17
+ # Options:
18
+ # * uri - the request uri
19
+ # * ip - the remote ip of the user. This is merged into the User context.
20
+ def scout_apm_trace(metric_name, options = {}, &block)
21
+ ScoutApm::Agent.instance.store.reset_transaction!
22
+ ScoutApm::Context.current.add_user(:ip => options[:ip]) if options[:ip]
23
+ instrument(metric_name, options) do
24
+ Thread::current[:scout_apm_scope_name] = metric_name
25
+ yield
26
+ Thread::current[:scout_apm_scope_name] = nil
27
+ end
28
+ # The context is cleared after instrumentation (rather than before) as tracing controller-actions doesn't occur until the controller-action is called.
29
+ # It does not trace before filters, which is a likely spot to add context. This means that any context applied during before_filters would be cleared.
30
+ ScoutApm::Context.clear!
31
+ end
32
+
33
+ # Options:
34
+ # * :scope - If specified, sets the sub-scope for the metric. We allow additional scope level. This is used
35
+ # * uri - the request uri
36
+ # when rendering the transaction tree in the UI.
37
+ def instrument(metric_name, options={}, &block)
38
+ # don't instrument if (1) NOT inside a transaction and (2) NOT a Controller metric.
39
+ if !Thread::current[:scout_apm_scope_name] and metric_name !~ /\AController\//
40
+ return yield
41
+ end
42
+ if options.delete(:scope)
43
+ Thread::current[:scout_apm_sub_scope] = metric_name
44
+ end
45
+ stack_item = ScoutApm::Agent.instance.store.record(metric_name)
46
+ begin
47
+ yield
48
+ ensure
49
+ Thread::current[:scout_apm_sub_scope] = nil if Thread::current[:scout_apm_sub_scope] == metric_name
50
+ ScoutApm::Agent.instance.store.stop_recording(stack_item,options)
51
+ end
52
+ end
53
+
54
+ def instrument_method(method,options = {})
55
+ ScoutApm::Agent.instance.logger.info "Instrumenting #{method}"
56
+ metric_name = options[:metric_name] || default_metric_name(method)
57
+ return if !instrumentable?(method) or instrumented?(method,metric_name)
58
+ class_eval instrumented_method_string(method, {:metric_name => metric_name, :scope => options[:scope]}), __FILE__, __LINE__
59
+
60
+ alias_method _uninstrumented_method_name(method, metric_name), method
61
+ alias_method method, _instrumented_method_name(method, metric_name)
62
+ end
63
+
64
+ private
65
+
66
+ def instrumented_method_string(method, options)
67
+ klass = (self === Module) ? "self" : "self.class"
68
+ "def #{_instrumented_method_name(method, options[:metric_name])}(*args, &block)
69
+ result = #{klass}.instrument(\"#{options[:metric_name]}\",{:scope => #{options[:scope] || false}}) do
70
+ #{_uninstrumented_method_name(method, options[:metric_name])}(*args, &block)
71
+ end
72
+ result
73
+ end"
74
+ end
75
+
76
+ # The method must exist to be instrumented.
77
+ def instrumentable?(method)
78
+ exists = method_defined?(method) || private_method_defined?(method)
79
+ ScoutApm::Agent.instance.logger.warn "The method [#{self.name}##{method}] does not exist and will not be instrumented" unless exists
80
+ exists
81
+ end
82
+
83
+ # +True+ if the method is already instrumented.
84
+ def instrumented?(method,metric_name)
85
+ instrumented = method_defined?(_instrumented_method_name(method, metric_name))
86
+ ScoutApm::Agent.instance.logger.warn "The method [#{self.name}##{method}] has already been instrumented" if instrumented
87
+ instrumented
88
+ end
89
+
90
+ def default_metric_name(method)
91
+ "Custom/#{self.name}/#{method.to_s}"
92
+ end
93
+
94
+ # given a method and a metric, this method returns the
95
+ # untraced alias of the method name
96
+ def _uninstrumented_method_name(method, metric_name)
97
+ "#{_sanitize_name(method)}_without_scout_instrument_#{_sanitize_name(metric_name)}"
98
+ end
99
+
100
+ # given a method and a metric, this method returns the traced
101
+ # alias of the method name
102
+ def _instrumented_method_name(method, metric_name)
103
+ name = "#{_sanitize_name(method)}_with_scout_instrument_#{_sanitize_name(metric_name)}"
104
+ end
105
+
106
+ # Method names like +any?+ or +replace!+ contain a trailing character that would break when
107
+ # eval'd as ? and ! aren't allowed inside method names.
108
+ def _sanitize_name(name)
109
+ name.to_s.tr_s('^a-zA-Z0-9', '_')
110
+ end
111
+ end # ClassMethods
112
+ end # module Tracer
@@ -0,0 +1,3 @@
1
+ module ScoutApm
2
+ VERSION = "0.1"
3
+ end
data/lib/scout_apm.rb ADDED
@@ -0,0 +1,41 @@
1
+ module ScoutApm
2
+ end
3
+ require 'socket'
4
+ require 'set'
5
+ require 'net/http'
6
+ require 'openssl'
7
+ require 'logger'
8
+ require 'yaml'
9
+ require 'cgi'
10
+
11
+ require File.expand_path('../scout_apm/version.rb', __FILE__)
12
+ require File.expand_path('../scout_apm/agent.rb', __FILE__)
13
+ require File.expand_path('../scout_apm/agent/logging.rb', __FILE__)
14
+ require File.expand_path('../scout_apm/agent/reporting.rb', __FILE__)
15
+ require File.expand_path('../scout_apm/layaway.rb', __FILE__)
16
+ require File.expand_path('../scout_apm/layaway_file.rb', __FILE__)
17
+ require File.expand_path('../scout_apm/config.rb', __FILE__)
18
+ require File.expand_path('../scout_apm/background_worker.rb', __FILE__)
19
+ require File.expand_path('../scout_apm/environment.rb', __FILE__)
20
+ require File.expand_path('../scout_apm/metric_meta.rb', __FILE__)
21
+ require File.expand_path('../scout_apm/metric_stats.rb', __FILE__)
22
+ require File.expand_path('../scout_apm/stack_item.rb', __FILE__)
23
+ require File.expand_path('../scout_apm/store.rb', __FILE__)
24
+ require File.expand_path('../scout_apm/tracer.rb', __FILE__)
25
+ require File.expand_path('../scout_apm/context.rb', __FILE__)
26
+ require File.expand_path('../scout_apm/slow_transaction.rb', __FILE__)
27
+ require File.expand_path('../scout_apm/instruments/process/process_cpu.rb', __FILE__)
28
+ require File.expand_path('../scout_apm/instruments/process/process_memory.rb', __FILE__)
29
+
30
+ if defined?(Rails) and Rails.respond_to?(:version) and Rails.version >= '3'
31
+ module ScoutApm
32
+ class Railtie < Rails::Railtie
33
+ initializer "scout_apm.start" do |app|
34
+ ScoutApm::Agent.instance.start
35
+ end
36
+ end
37
+ end
38
+ else
39
+ ScoutApm::Agent.instance.start
40
+ end
41
+
data/scout_apm.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "scout_apm/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "scout_apm"
7
+ s.version = ScoutApm::VERSION
8
+ s.authors = ["Derek Haynes",'Andre Lewis']
9
+ s.email = ["support@scoutapp.com"]
10
+ s.homepage = "https://github.com/scoutapp/scout_apm_ruby"
11
+ s.summary = "Ruby application performance monitoring"
12
+ s.description = "Monitors Ruby apps and reports detailed metrics on performance to Scout."
13
+
14
+ s.rubyforge_project = "scout_apm"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib","data"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ # s.add_runtime_dependency "rest-client"
24
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scout_apm
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Derek Haynes
8
+ - Andre Lewis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-07-22 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Monitors Ruby apps and reports detailed metrics on performance to Scout.
15
+ email:
16
+ - support@scoutapp.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".gitignore"
22
+ - CHANGELOG.markdown
23
+ - Gemfile
24
+ - README.markdown
25
+ - Rakefile
26
+ - data/cacert.pem
27
+ - lib/scout_apm.rb
28
+ - lib/scout_apm/agent.rb
29
+ - lib/scout_apm/agent/logging.rb
30
+ - lib/scout_apm/agent/reporting.rb
31
+ - lib/scout_apm/background_worker.rb
32
+ - lib/scout_apm/config.rb
33
+ - lib/scout_apm/context.rb
34
+ - lib/scout_apm/environment.rb
35
+ - lib/scout_apm/instruments/active_record_instruments.rb
36
+ - lib/scout_apm/instruments/mongoid_instruments.rb
37
+ - lib/scout_apm/instruments/moped_instruments.rb
38
+ - lib/scout_apm/instruments/net_http.rb
39
+ - lib/scout_apm/instruments/process/process_cpu.rb
40
+ - lib/scout_apm/instruments/process/process_memory.rb
41
+ - lib/scout_apm/instruments/rails/action_controller_instruments.rb
42
+ - lib/scout_apm/instruments/rails3_or_4/action_controller_instruments.rb
43
+ - lib/scout_apm/layaway.rb
44
+ - lib/scout_apm/layaway_file.rb
45
+ - lib/scout_apm/metric_meta.rb
46
+ - lib/scout_apm/metric_stats.rb
47
+ - lib/scout_apm/slow_transaction.rb
48
+ - lib/scout_apm/stack_item.rb
49
+ - lib/scout_apm/store.rb
50
+ - lib/scout_apm/tracer.rb
51
+ - lib/scout_apm/version.rb
52
+ - scout_apm.gemspec
53
+ homepage: https://github.com/scoutapp/scout_apm_ruby
54
+ licenses: []
55
+ metadata: {}
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ - data
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project: scout_apm
73
+ rubygems_version: 2.4.6
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: Ruby application performance monitoring
77
+ test_files: []
78
+ has_rdoc: