scout_apm 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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