scout_apm 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +4 -0
  3. data/lib/scout_apm.rb +48 -23
  4. data/lib/scout_apm/agent.rb +93 -130
  5. data/lib/scout_apm/agent/reporting.rb +34 -63
  6. data/lib/scout_apm/app_server_load.rb +29 -0
  7. data/lib/scout_apm/background_worker.rb +6 -6
  8. data/lib/scout_apm/capacity.rb +48 -48
  9. data/lib/scout_apm/config.rb +5 -5
  10. data/lib/scout_apm/context.rb +3 -3
  11. data/lib/scout_apm/environment.rb +64 -100
  12. data/lib/scout_apm/framework_integrations/rails_2.rb +32 -0
  13. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +33 -0
  14. data/lib/scout_apm/framework_integrations/ruby.rb +26 -0
  15. data/lib/scout_apm/framework_integrations/sinatra.rb +27 -0
  16. data/lib/scout_apm/instruments/active_record_instruments.rb +1 -1
  17. data/lib/scout_apm/instruments/mongoid_instruments.rb +1 -1
  18. data/lib/scout_apm/instruments/moped_instruments.rb +3 -3
  19. data/lib/scout_apm/instruments/net_http.rb +2 -2
  20. data/lib/scout_apm/instruments/process/process_cpu.rb +41 -20
  21. data/lib/scout_apm/instruments/process/process_memory.rb +45 -30
  22. data/lib/scout_apm/instruments/rails/action_controller_instruments.rb +20 -18
  23. data/lib/scout_apm/instruments/rails3_or_4/action_controller_instruments.rb +17 -14
  24. data/lib/scout_apm/layaway.rb +12 -12
  25. data/lib/scout_apm/layaway_file.rb +1 -1
  26. data/lib/scout_apm/metric_meta.rb +9 -9
  27. data/lib/scout_apm/metric_stats.rb +8 -8
  28. data/lib/scout_apm/reporter.rb +83 -0
  29. data/lib/scout_apm/serializers/app_server_load_serializer.rb +15 -0
  30. data/lib/scout_apm/serializers/directive_serializer.rb +15 -0
  31. data/lib/scout_apm/serializers/payload_serializer.rb +14 -0
  32. data/lib/scout_apm/server_integrations/null.rb +30 -0
  33. data/lib/scout_apm/server_integrations/passenger.rb +35 -0
  34. data/lib/scout_apm/server_integrations/puma.rb +30 -0
  35. data/lib/scout_apm/server_integrations/rainbows.rb +36 -0
  36. data/lib/scout_apm/server_integrations/thin.rb +41 -0
  37. data/lib/scout_apm/server_integrations/unicorn.rb +35 -0
  38. data/lib/scout_apm/server_integrations/webrick.rb +25 -0
  39. data/lib/scout_apm/slow_transaction.rb +3 -3
  40. data/lib/scout_apm/stack_item.rb +19 -17
  41. data/lib/scout_apm/store.rb +35 -35
  42. data/lib/scout_apm/tracer.rb +121 -110
  43. data/lib/scout_apm/utils/sql_sanitizer.rb +2 -2
  44. data/lib/scout_apm/version.rb +2 -1
  45. data/test/unit/environment_test.rb +5 -5
  46. metadata +18 -2
@@ -0,0 +1,29 @@
1
+ module ScoutApm
2
+ class AppServerLoad
3
+ attr_reader :logger
4
+
5
+ def initialize(logger: Agent.instance.logger)
6
+ @logger = logger
7
+ end
8
+
9
+ def run
10
+ logger.info("Sending Startup Info: #{data.inspect}")
11
+ payload = ScoutApm::Serializers::AppServerLoadSerializer.serialize(data)
12
+ reporter = Reporter.new(type: :app_server_load)
13
+ reporter.report(payload)
14
+ rescue => e
15
+ logger.debug("Failed Startup Info - #{e.message} \n\t#{e.backtrace.join("\t\n")}")
16
+ end
17
+
18
+ def data
19
+ { server_time: Time.now,
20
+ framework: ScoutApm::Environment.instance.framework_integration.name,
21
+ framework_version: ScoutApm::Environment.instance.framework_integration.version,
22
+ ruby_version: RUBY_VERSION,
23
+ hostname: ScoutApm::Environment.instance.hostname,
24
+ database_engine: ScoutApm::Environment.instance.database_engine,
25
+ application_name: ScoutApm::Environment.instance.application_name,
26
+ }
27
+ end
28
+ end
29
+ end
@@ -1,21 +1,21 @@
1
- # Used to run a given task every 60 seconds.
1
+ # Used to run a given task every 60 seconds.
2
2
  class ScoutApm::BackgroundWorker
3
3
  # in seconds, time between when the worker thread wakes up and runs.
4
4
  PERIOD = 60
5
-
5
+
6
6
  def initialize
7
7
  @keep_running = true
8
8
  end
9
-
9
+
10
10
  def stop
11
11
  @keep_running = false
12
12
  end
13
-
13
+
14
14
  # Runs the task passed to +start+ once.
15
15
  def run_once
16
16
  @task.call if @task
17
17
  end
18
-
18
+
19
19
  # Starts running the passed block every 60 seconds (starting now).
20
20
  def start(&block)
21
21
  @task = block
@@ -40,4 +40,4 @@ class ScoutApm::BackgroundWorker
40
40
  ScoutApm::Agent.instance.logger.debug $!.backtrace
41
41
  end
42
42
  end
43
- end
43
+ end
@@ -1,54 +1,54 @@
1
1
  # Encapsulates logic for determining capacity utilization of the Ruby processes.
2
2
  class ScoutApm::Capacity
3
- attr_reader :processing_start_time, :accumulated_time, :transaction_entry_time
3
+ attr_reader :processing_start_time, :accumulated_time, :transaction_entry_time
4
4
 
5
- def initialize
6
- @processing_start_time = Time.now
7
- @lock ||= Mutex.new # the transaction_entry_time could be modified while processing a request or when #process is called.
8
- @accumulated_time = 0.0
9
- end
10
-
11
- # Called when a transaction is traced.
12
- def start_transaction!
13
- @lock.synchronize do
14
- @transaction_entry_time = Time.now
15
- end
16
- end
5
+ def initialize
6
+ @processing_start_time = Time.now
7
+ @lock ||= Mutex.new # the transaction_entry_time could be modified while processing a request or when #process is called.
8
+ @accumulated_time = 0.0
9
+ end
17
10
 
18
- # Called when a transaction completes to record its time used.
19
- def finish_transaction!
20
- @lock.synchronize do
21
- if transaction_entry_time
22
- @accumulated_time += (Time.now - transaction_entry_time).to_f
23
- else
24
- ScoutApm::Agent.instance.logger.warn "No transaction entry time. Not recording capacity metrics for transaction."
25
- end
26
- @transaction_entry_time = nil
27
- end
28
- end
11
+ # Called when a transaction is traced.
12
+ def start_transaction!
13
+ @lock.synchronize do
14
+ @transaction_entry_time = Time.now
15
+ end
16
+ end
29
17
 
30
- # Ran when sending metrics to server. Reports capacity usage metrics.
31
- def process
32
- process_time = Time.now
33
- ScoutApm::Agent.instance.logger.debug "Processing capacity usage for [#{@processing_start_time}] to [#{process_time}]. Time Spent: #{@accumulated_time}."
34
- @lock.synchronize do
35
- time_spent = @accumulated_time
36
- @accumulated_time = 0.0
37
- # If a transaction is still running, capture its running time up to now and
38
- # reset the +transaction_entry_time+ to now.
39
- if @transaction_entry_time
40
- time_spent += (process_time - @transaction_entry_time).to_f
41
- ScoutApm::Agent.instance.logger.debug "A transaction is running while calculating capacity. Start time: [#{transaction_entry_time}]. Will update the entry time to [#{process_time}]."
42
- @transaction_entry_time = process_time # prevent from over-counting capacity usage. update the transaction start time to now.
43
- end
44
- time_spent = 0.0 if time_spent < 0.0
18
+ # Called when a transaction completes to record its time used.
19
+ def finish_transaction!
20
+ @lock.synchronize do
21
+ if transaction_entry_time
22
+ @accumulated_time += (Time.now - transaction_entry_time).to_f
23
+ else
24
+ ScoutApm::Agent.instance.logger.warn "No transaction entry time. Not recording capacity metrics for transaction."
25
+ end
26
+ @transaction_entry_time = nil
27
+ end
28
+ end
45
29
 
46
- window = (process_time - processing_start_time).to_f # time period we are evaulating capacity usage.
47
- window = 1.0 if window <= 0.0 # prevent divide-by-zero if clock adjusted.
48
- capacity = time_spent / window
49
- ScoutApm::Agent.instance.logger.debug "Instance/Capacity: #{capacity}"
50
- ScoutApm::Agent.instance.store.track!("Instance/Capacity",capacity,:scope => nil)
51
- @processing_start_time = process_time
52
- end
53
- end
54
- end
30
+ # Ran when sending metrics to server. Reports capacity usage metrics.
31
+ def process
32
+ process_time = Time.now
33
+ ScoutApm::Agent.instance.logger.debug "Processing capacity usage for [#{@processing_start_time}] to [#{process_time}]. Time Spent: #{@accumulated_time}."
34
+ @lock.synchronize do
35
+ time_spent = @accumulated_time
36
+ @accumulated_time = 0.0
37
+ # If a transaction is still running, capture its running time up to now and
38
+ # reset the +transaction_entry_time+ to now.
39
+ if @transaction_entry_time
40
+ time_spent += (process_time - @transaction_entry_time).to_f
41
+ ScoutApm::Agent.instance.logger.debug "A transaction is running while calculating capacity. Start time: [#{transaction_entry_time}]. Will update the entry time to [#{process_time}]."
42
+ @transaction_entry_time = process_time # prevent from over-counting capacity usage. update the transaction start time to now.
43
+ end
44
+ time_spent = 0.0 if time_spent < 0.0
45
+
46
+ window = (process_time - processing_start_time).to_f # time period we are evaulating capacity usage.
47
+ window = 1.0 if window <= 0.0 # prevent divide-by-zero if clock adjusted.
48
+ capacity = time_spent / window
49
+ ScoutApm::Agent.instance.logger.debug "Instance/Capacity: #{capacity}"
50
+ ScoutApm::Agent.instance.store.track!("Instance/Capacity",capacity,:scope => nil)
51
+ @processing_start_time = process_time
52
+ end
53
+ end
54
+ end
@@ -6,8 +6,8 @@ require 'scout_apm/environment'
6
6
  module ScoutApm
7
7
  class Config
8
8
  DEFAULTS = {
9
- 'host' => 'https://apm.scoutapp.com',
10
- 'log_level' => 'info'
9
+ 'host' => 'https://apm.scoutapp.com',
10
+ 'log_level' => 'info',
11
11
  }.freeze
12
12
 
13
13
  def initialize(config_path = nil)
@@ -25,7 +25,7 @@ module ScoutApm
25
25
  private
26
26
 
27
27
  def config_path
28
- @config_path || File.join(ScoutApm::Environment.new.root, "config", "scout_apm.yml")
28
+ @config_path || File.join(ScoutApm::Environment.instance.root, "config", "scout_apm.yml")
29
29
  end
30
30
 
31
31
  def config_file
@@ -37,14 +37,14 @@ module ScoutApm
37
37
  end
38
38
 
39
39
  def config_environment
40
- @config_environment ||= ScoutApm::Environment.new.env
40
+ @config_environment ||= ScoutApm::Environment.instance.env
41
41
  end
42
42
 
43
43
  def load_file
44
44
  settings_hash = {}
45
45
  begin
46
46
  if File.exist?(config_file)
47
- settings_hash = YAML.load(ERB.new(File.read(config_file)).result(binding))[config_environment] || {}
47
+ settings_hash = YAML.load(ERB.new(File.read(config_file)).result(binding))[config_environment] || {}
48
48
  else
49
49
  logger.warn "No config file found at [#{config_file}]."
50
50
  end
@@ -10,7 +10,7 @@ class ScoutApm::Context
10
10
  @user = {}
11
11
  end
12
12
 
13
- # Generates a hash representation of the Context.
13
+ # Generates a hash representation of the Context.
14
14
  # Example: {:monthly_spend => 100, :user => {:ip => '127.0.0.1'}}
15
15
  def to_hash
16
16
  @extra.merge({:user => @user})
@@ -77,7 +77,7 @@ class ScoutApm::Context
77
77
 
78
78
  # take the entire Hash vs. just the value so the logger output is more helpful on error.
79
79
  def value_valid?(key_value)
80
- # ensure one of our accepted types.
80
+ # ensure one of our accepted types.
81
81
  value = key_value.values.last
82
82
  if !valid_type?([String, Symbol, Numeric, Time, Date, TrueClass, FalseClass],value)
83
83
  ScoutApm::Agent.instance.logger.warn "The value for [#{key_value.keys.first}] is not a valid type [#{value.class}]."
@@ -102,4 +102,4 @@ class ScoutApm::Context
102
102
  end
103
103
  true
104
104
  end
105
- end
105
+ end
@@ -1,68 +1,77 @@
1
+ require 'singleton'
2
+
1
3
  # Used to retrieve environment information for this application.
2
4
  module ScoutApm
3
5
  class Environment
6
+ include Singleton
7
+
8
+ # I've put Thin and Webrick last as they are often used in development and included in Gemfiles
9
+ # but less likely used in production.
10
+ SERVER_INTEGRATIONS = [
11
+ ScoutApm::ServerIntegrations::Passenger.new(Logger.new(STDOUT)),
12
+ ScoutApm::ServerIntegrations::Unicorn.new(Logger.new(STDOUT)),
13
+ ScoutApm::ServerIntegrations::Rainbows.new(Logger.new(STDOUT)),
14
+ ScoutApm::ServerIntegrations::Puma.new(Logger.new(STDOUT)),
15
+ ScoutApm::ServerIntegrations::Thin.new(Logger.new(STDOUT)),
16
+ ScoutApm::ServerIntegrations::Webrick.new(Logger.new(STDOUT)),
17
+ ScoutApm::ServerIntegrations::Null.new(Logger.new(STDOUT)), # must be last
18
+ ]
19
+
20
+ FRAMEWORK_INTEGRATIONS = [
21
+ ScoutApm::FrameworkIntegrations::Rails2.new,
22
+ ScoutApm::FrameworkIntegrations::Rails3Or4.new,
23
+ ScoutApm::FrameworkIntegrations::Sinatra.new,
24
+ ScoutApm::FrameworkIntegrations::Ruby.new, # Fallback if none match
25
+ ]
26
+
4
27
  def env
5
- @env ||= case framework
6
- when :rails
7
- RAILS_ENV.dup
8
- when :rails3_or_4
9
- Rails.env
10
- when :sinatra
11
- ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
12
- when :ruby
13
- ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
14
- end
28
+ @env ||= framework_integration.env
15
29
  end
16
30
 
17
31
  def framework
18
- @framework ||= case
19
- when defined?(::Rails) && defined?(ActionController)
20
- if Rails::VERSION::MAJOR < 3
21
- :rails
22
- else
23
- :rails3_or_4
24
- end
25
- when defined?(::Sinatra) && defined?(::Sinatra::Base) then :sinatra
26
- else :ruby
27
- end
32
+ framework_integration.name
33
+ end
34
+
35
+ def framework_integration
36
+ @framework ||= FRAMEWORK_INTEGRATIONS.detect{ |integration| integration.present? }
37
+ end
38
+
39
+ def application_name
40
+ Agent.instance.config.value("name") || framework_integration.application_name
28
41
  end
29
42
 
30
43
  def database_engine
31
44
  default = :mysql
32
45
 
33
46
  if defined?(ActiveRecord::Base)
34
- return default unless ActiveRecord::Base.connected?
35
-
36
- case ActiveRecord::Base.connection.class.to_s
37
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
38
- :mysql
39
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
40
- :postgres
41
- when "ActiveRecord::ConnectionAdapters::SQLite3Adapter"
42
- :sqlite
47
+ config = ActiveRecord::Base.connection_config
48
+ if config && config[:adapter]
49
+ case config[:adapter]
50
+ when "postgres" then :postgres
51
+ when "sqlite3" then :sqlite
52
+ when "mysql" then :mysql
53
+ else default
54
+ end
43
55
  else
44
56
  default
45
57
  end
46
58
  else
47
- # TODO: detection outside of Rails
59
+ # TODO: Figure out how to detect outside of Rails context. (sequel, ROM, etc)
48
60
  default
49
61
  end
50
62
  end
51
63
 
52
64
  def processors
53
- return @processors if @processors
54
- unless @processors
55
- proc_file = '/proc/cpuinfo'
56
- if !File.exist?(proc_file)
57
- @processors = 1
58
- elsif `cat #{proc_file} | grep 'model name' | wc -l` =~ /(\d+)/
59
- @processors = $1.to_i
60
- end
61
- if @processors < 1
62
- @processors = 1
63
- end
64
- end
65
- @processors
65
+ @processors ||= begin
66
+ proc_file = '/proc/cpuinfo'
67
+ processors = if !File.exist?(proc_file)
68
+ 1
69
+ else
70
+ lines = File.read("/proc/cpuinfo").lines.to_a
71
+ lines.grep(/^processor\s*:/i).size
72
+ end
73
+ [processors, 1].compact.max
74
+ end
66
75
  end
67
76
 
68
77
  def root
@@ -82,73 +91,28 @@ module ScoutApm
82
91
  end
83
92
 
84
93
  def hostname
85
- heroku? ? ENV['DYNO'] : Socket.gethostname
94
+ @hostname ||= heroku? ? ENV['DYNO'] : Socket.gethostname
86
95
  end
87
96
 
97
+
98
+ # Returns the whole integration object
88
99
  # This needs to be improved. Frequently, multiple app servers gem are present and which
89
100
  # ever is checked first becomes the designated app server.
90
101
  #
91
- # I've put Thin and Webrick last as they are often used in development and included in Gemfiles
92
- # but less likely used in production.
93
- #
94
- # Next step: (1) list out all detected app servers (2) install hooks for those that need it (passenger, rainbows, unicorn).
95
- #
96
- # Believe the biggest downside is the master process for forking app servers will get a background worker. Not sure how this will
97
- # impact metrics (it shouldn't process requests).
98
- def app_server
99
- @app_server ||= if passenger? then :passenger
100
- elsif rainbows? then :rainbows
101
- elsif unicorn? then :unicorn
102
- elsif puma? then :puma
103
- elsif thin? then :thin
104
- elsif webrick? then :webrick
105
- else nil
106
- end
107
- end
108
-
109
- ### app server related-checks
110
- def thin?
111
- if defined?(::Thin) && defined?(::Thin::Server)
112
- # Ensure Thin is actually initialized. It could just be required and not running.
113
- ObjectSpace.each_object(Thin::Server) { |x| return true }
114
- false
115
- end
116
- end
117
-
118
- # Called via +#forking?+ since Passenger forks. Adds an event listener to start the worker thread
119
- # inside the passenger worker process.
120
- # Background: http://www.modrails.com/documentation/Users%20guide%20Nginx.html#spawning%5Fmethods%5Fexplained
121
- def passenger?
122
- (defined?(::Passenger) && defined?(::Passenger::AbstractServer)) || defined?(::PhusionPassenger)
102
+ # Next step: (1) list out all detected app servers (2) install hooks for those that need it (passenger, rainbows, unicorn).
103
+ def app_server_integration
104
+ @app_server = SERVER_INTEGRATIONS.detect{ |integration| integration.present? }
123
105
  end
124
106
 
125
- def webrick?
126
- defined?(::WEBrick) && defined?(::WEBrick::VERSION)
127
- end
128
-
129
- def rainbows?
130
- if defined?(::Rainbows) && defined?(::Rainbows::HttpServer)
131
- ObjectSpace.each_object(::Rainbows::HttpServer) { |x| return true }
132
- false
133
- end
134
- end
135
-
136
- def unicorn?
137
- if defined?(::Unicorn) && defined?(::Unicorn::HttpServer)
138
- # Ensure Unicorn is actually initialized. It could just be required and not running.
139
- ObjectSpace.each_object(::Unicorn::HttpServer) { |x| return true }
140
- false
141
- end
142
- end
143
-
144
- def puma?
145
- defined?(::Puma) && File.basename($0) == 'puma'
107
+ # App server's name (symbol)
108
+ def app_server
109
+ app_server_integration.name
146
110
  end
147
111
 
148
- # If forking, don't start worker thread in the master process. Since it's started as a Thread, it won't survive
149
- # the fork.
112
+ # If forking, don't start worker thread in the master process. Since it's
113
+ # started as a Thread, it won't survive the fork.
150
114
  def forking?
151
- passenger? or unicorn? or rainbows? or puma?
115
+ app_server_integration.forking?
152
116
  end
153
117
 
154
118
  ### ruby checks
@@ -0,0 +1,32 @@
1
+ module ScoutApm
2
+ module FrameworkIntegrations
3
+ class Rails2
4
+ def name
5
+ :rails
6
+ end
7
+
8
+ def version
9
+ Rails::VERSION::STRING
10
+ end
11
+
12
+ def present?
13
+ defined?(::Rails) &&
14
+ defined?(ActionController) &&
15
+ Rails::VERSION::MAJOR < 3
16
+ end
17
+
18
+ def application_name
19
+ if defined?(::Rails)
20
+ ::Rails.application.class.to_s
21
+ .sub(/::Application$/, '')
22
+ end
23
+ rescue
24
+ nil
25
+ end
26
+
27
+ def env
28
+ RAILS_ENV.dup
29
+ end
30
+ end
31
+ end
32
+ end