tracebin 0.0.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +42 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/dump.rdb +0 -0
  13. data/lib/vizsla/agent.rb +87 -0
  14. data/lib/vizsla/background_job_instrumentation/active_job.rb +14 -0
  15. data/lib/vizsla/background_job_instrumentation/resque.rb +37 -0
  16. data/lib/vizsla/background_job_instrumentation/sidekiq.rb +19 -0
  17. data/lib/vizsla/background_job_instrumentation.rb +34 -0
  18. data/lib/vizsla/background_timer.rb +9 -0
  19. data/lib/vizsla/config.rb +21 -0
  20. data/lib/vizsla/events.rb +67 -0
  21. data/lib/vizsla/health_monitor.rb +24 -0
  22. data/lib/vizsla/helpers.rb +7 -0
  23. data/lib/vizsla/logger.rb +55 -0
  24. data/lib/vizsla/middleware.rb +51 -0
  25. data/lib/vizsla/patches/action_view_layout.rb +34 -0
  26. data/lib/vizsla/patches/mysql2.rb +22 -0
  27. data/lib/vizsla/patches/postgres.rb +42 -0
  28. data/lib/vizsla/patches/sidekiq_health.rb +27 -0
  29. data/lib/vizsla/patches/sinatra.rb +21 -0
  30. data/lib/vizsla/patches.rb +44 -0
  31. data/lib/vizsla/puppet_master.rb +17 -0
  32. data/lib/vizsla/recorder.rb +43 -0
  33. data/lib/vizsla/reporter.rb +74 -0
  34. data/lib/vizsla/storage.rb +30 -0
  35. data/lib/vizsla/subscribers.rb +172 -0
  36. data/lib/vizsla/system_health_sample.rb +187 -0
  37. data/lib/vizsla/timer.rb +60 -0
  38. data/lib/vizsla/version.rb +3 -0
  39. data/lib/vizsla/worker_process_monitor.rb +25 -0
  40. data/lib/vizsla.rb +17 -0
  41. data/vizsla.gemspec +29 -0
  42. metadata +154 -0
@@ -0,0 +1,22 @@
1
+ ::Mysql2::Client.class_eval do
2
+ alias_method :query_without_vizsla, :query
3
+
4
+ def query(*args, &block)
5
+ start_time = Time.now
6
+ result = query_without_vizsla(*args, &block)
7
+ end_time = Time.now
8
+
9
+ event_data = [
10
+ 'sql.mysql2_query',
11
+ start_time,
12
+ end_time,
13
+ {
14
+ sql: args[0]
15
+ }
16
+ ]
17
+
18
+ ::Vizsla::Patches.handle_event :mysql2, event_data
19
+
20
+ result
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ ::PG::Connection.class_eval do
2
+ alias_method :exec_without_vizsla, :exec
3
+ alias_method :exec_params_without_vizsla, :exec_params
4
+
5
+ def exec_params(*args, &block)
6
+ start_time = Time.now
7
+ result = exec_params_without_vizsla(*args, &block)
8
+ end_time = Time.now
9
+
10
+ event_data = [
11
+ 'sql.postgres_exec',
12
+ start_time,
13
+ end_time,
14
+ {
15
+ sql: args[0]
16
+ }
17
+ ]
18
+
19
+ ::Vizsla::Patches.handle_event :postgres, event_data
20
+
21
+ result
22
+ end
23
+
24
+ def exec(*args, &block)
25
+ start_time = Time.now
26
+ result = exec_without_vizsla(*args, &block)
27
+ end_time = Time.now
28
+
29
+ event_data = [
30
+ 'sql.postgres_exec',
31
+ start_time,
32
+ end_time,
33
+ {
34
+ sql: args[0]
35
+ }
36
+ ]
37
+
38
+ ::Vizsla::Patches.handle_event :postgres, event_data
39
+
40
+ result
41
+ end
42
+ end
@@ -0,0 +1,27 @@
1
+ require 'vizsla/patches'
2
+ require 'vizsla/system_health_sample'
3
+ require 'concurrent'
4
+
5
+ require 'sidekiq/launcher'
6
+
7
+ ::Sidekiq::Launcher.class_eval do
8
+ alias_method :run_without_vizsla, :run
9
+ alias_method :stop_without_vizsla, :stop
10
+
11
+ def run
12
+ @vizsla_task = Concurrent::TimerTask.new(execution_interval: 10) do
13
+ health = Vizsla::SystemHealthSample.new process: :worker
14
+ ::Vizsla::Patches.handle_event :sidekiq_health, health
15
+ end
16
+
17
+ @vizsla_task.execute
18
+
19
+ run_without_vizsla
20
+ end
21
+
22
+ def stop
23
+ @vizsla_task.shutdown
24
+
25
+ stop_without_vizsla
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ ::Sinatra::Base.class_eval do
2
+ alias_method :dispatch_without_vizsla!, :dispatch!
3
+
4
+ def dispatch!(*args, &block)
5
+ start_time = Time.now
6
+ result = dispatch_without_vizsla!(*args, *block)
7
+ end_time = Time.now
8
+ route = env['sinatra.route']
9
+
10
+ event_data = [
11
+ 'sinatra.route_exec',
12
+ start_time,
13
+ end_time,
14
+ route
15
+ ]
16
+
17
+ ::Vizsla::Patches.handle_event :sinatra, event_data
18
+
19
+ result
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ require 'vizsla/helpers'
2
+
3
+ module Vizsla
4
+ ##
5
+ # This singleton class handles patching for any given library we wish to
6
+ # instrument. To create a new patch for a library, just create a file in the
7
+ # +lib/patches+ directory with any name. These files typically contain code
8
+ # that will monkeypatch a given library. When you wish to execute the code
9
+ # in that file, just call its corresponding +patch_+ method. For example, if
10
+ # we have a file +lib/patches/foo.rb+, then we would just call:
11
+ #
12
+ # ::Vizsla::Patches.patch_foo
13
+ #
14
+ class Patches
15
+ include ::Vizsla::Helpers
16
+
17
+ PATCH_METHOD_REGEX = /^patch_(.*)$/
18
+
19
+ class << self
20
+ def handle_event(handler_name, event_data)
21
+ handler = instance_variable_get "@#{handler_name}_event_handler"
22
+ handler.call event_data unless handler.nil?
23
+ end
24
+
25
+ def method_missing(method_sym, *args, &block)
26
+ if method_sym.to_s =~ PATCH_METHOD_REGEX
27
+ patch_name = $1
28
+ instance_variable_set "@#{patch_name}_event_handler", block
29
+ require "vizsla/patches/#{patch_name}"
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ def respond_to?(method_sym, include_private = false)
36
+ if method_sym.to_s =~ PATCH_METHOD_REGEX
37
+ true
38
+ else
39
+ super
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ require 'vizsla/logger'
2
+ require 'vizsla/reporter'
3
+
4
+ module Vizsla
5
+ class PuppetMaster
6
+ def initialize(puppet, options = {})
7
+ @puppet = puppet
8
+ @logger = RequestLogger.new(options[:logger])
9
+ @storage = ::Vizsla::Agent.storage
10
+ end
11
+
12
+ def process
13
+ # @logger.display_payload @puppet.payload
14
+ @storage << @puppet.payload
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ module Vizsla
2
+ class Recorder
3
+ THREAD_LOCAL_KEY = :_vizsla_current
4
+ LOCK = Mutex.new
5
+
6
+ class << self
7
+ def current
8
+ LOCK.synchronize do
9
+ Thread.current[THREAD_LOCAL_KEY]
10
+ end
11
+ end
12
+
13
+ def current=(val)
14
+ Thread.current[THREAD_LOCAL_KEY] = val
15
+ end
16
+
17
+ def start_recording
18
+ self.current = {}
19
+ end
20
+
21
+ def recording?
22
+ !self.current.nil?
23
+ end
24
+
25
+ def add_event(event)
26
+ return unless self.recording?
27
+ self.current[:events] ||= []
28
+ self.current[:events] << event.data_hash
29
+ end
30
+ alias_method :<<, :add_event
31
+
32
+ def events
33
+ self.current[:events]
34
+ end
35
+
36
+ def stop_recording
37
+ LOCK.synchronize do
38
+ Thread.current[THREAD_LOCAL_KEY] = nil
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,74 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'concurrent'
4
+
5
+ module Vizsla
6
+ class Reporter
7
+ attr_reader :logger, :config, :storage
8
+
9
+ def initialize(storage = Vizsla::Agent.storage, config = Vizsla::Agent.config, logger = Vizsla::Agent.logger)
10
+ @logger = logger
11
+ @config = config
12
+ @storage = storage
13
+
14
+ host = Vizsla::Agent.config.host
15
+ path = Vizsla::Agent.config.report_path
16
+ @uri = URI("#{host}/#{path}")
17
+
18
+ @bin_id = Vizsla::Agent.config.bin_id
19
+ end
20
+
21
+ def start!
22
+ @task = Concurrent::TimerTask.new do
23
+ unless storage.unloaded?
24
+ payload = storage.unload
25
+ res = send_data payload
26
+
27
+ handle_response res, payload
28
+ end
29
+ end
30
+
31
+ logger.info 'TRACEBIN: Reporter starting.'
32
+ @task.execute
33
+ end
34
+
35
+ def stop!
36
+ logger.info 'TRACEBIN: Reporter stopping. The agent will no longer report metrics to the server.'
37
+ @task.shutdown if @task && @task.running?
38
+ end
39
+
40
+ private
41
+
42
+ def send_data(payload)
43
+ logger.info 'TRACEBIN: Sending analytics data to the server.'
44
+
45
+ Net::HTTP.start(@uri.host, @uri.port) do |http|
46
+ body = {
47
+ bin_id: @bin_id,
48
+ report: payload
49
+ }.to_json
50
+
51
+ req = Net::HTTP::Post.new @uri
52
+ req.content_type = 'application/json'
53
+ req.body = body
54
+
55
+ http.request req
56
+ end
57
+ rescue Exception => e
58
+ logger.warn "TRACEBIN: Exception occurred sending data to the server: #{e.message}"
59
+ end
60
+
61
+ def handle_response(res, payload)
62
+ case res
63
+ when Net::HTTPSuccess
64
+ logger.info 'TRACEBIN: Successfully sent payload to the server.'
65
+ when Net::HTTPBadRequest
66
+ logger.warn 'TRACEBIN: App bin ID not found. Please create a new app bin and add it to the config.'
67
+ self.stop!
68
+ else
69
+ logger.warn 'TRACEBIN: Failed to send data to the server. Will try again in 1 minute.'
70
+ @storage.add_payload payload
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,30 @@
1
+ require 'concurrent'
2
+
3
+ module Vizsla
4
+ class Storage
5
+ attr_reader :values
6
+
7
+ def initialize
8
+ @values = Concurrent::Array.new
9
+ end
10
+
11
+ def add(payload)
12
+ @values << payload
13
+ end
14
+ alias_method :<<, :add
15
+
16
+ def add_payload(payload)
17
+ @values += payload if payload.is_a?(Array)
18
+ end
19
+
20
+ def unload
21
+ duplicate_values = @values.dup
22
+ @values.clear
23
+ duplicate_values
24
+ end
25
+
26
+ def unloaded?
27
+ @values.empty?
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,172 @@
1
+ require 'vizsla/recorder'
2
+ require 'vizsla/patches'
3
+ require 'vizsla/events'
4
+ require 'vizsla/background_job_instrumentation'
5
+
6
+ module Vizsla
7
+ ##
8
+ # Subscribes to certain events, handels them, and passes event data to the
9
+ # +Recorder+ class. The general workflow goes like this:
10
+ #
11
+ # 1. Patch the method you want to profile. It should generate an array that
12
+ # looks like the following:
13
+ #
14
+ # [
15
+ # "event_type.event_domain",
16
+ # start_time,
17
+ # stop_time,
18
+ # etc...,
19
+ # { event: :data }
20
+ # ]
21
+ #
22
+ # Note that the event hash must be the last element in the array (this is to
23
+ # maintain consistency with ActiveSupport::Notifications).
24
+ #
25
+ # 2. Store that event array into an appropriate +Event+ subclass.
26
+ # 3. Add each +Event+ object to +@events_data+ using the +#<<+ method.
27
+ #
28
+ class Subscribers
29
+ def initialize
30
+ @events_data = Recorder
31
+ collect_events_data
32
+ end
33
+
34
+ private
35
+
36
+ def collect_events_data
37
+ if rails_app?
38
+ rails_hooks
39
+ else
40
+ other_hooks
41
+ end
42
+
43
+ background_job_hooks
44
+ end
45
+
46
+ def rails_hooks
47
+ sql_hook
48
+ process_action_hook
49
+ render_layout_hook
50
+ render_template_hook
51
+ render_partial_hook
52
+ end
53
+
54
+ def other_hooks
55
+ sinatra_hook if sinatra_app?
56
+ db_hooks
57
+ background_job_hooks
58
+ end
59
+
60
+ def background_job_hooks
61
+ if defined? ::ActiveJob
62
+ active_job_hook
63
+ else
64
+ sidekiq_hook
65
+ resque_hook
66
+ end
67
+ end
68
+
69
+ def db_hooks
70
+ postgres_hook
71
+ mysql2_hook
72
+ end
73
+
74
+ # ===---------------------------===
75
+ # Rails Hooks
76
+ # ===---------------------------===
77
+
78
+ def sql_hook
79
+ subscribe_asn 'sql.active_record', SQLEvent
80
+ end
81
+
82
+ def process_action_hook
83
+ subscribe_asn 'process_action.action_controller', ControllerEvent
84
+ end
85
+
86
+ def render_layout_hook
87
+ unless [ActionPack::VERSION::MAJOR, ActionPack::VERSION::MINOR] == [3, 0]
88
+ ::Vizsla::Patches.patch_action_view_layout do |event_data|
89
+ event = ViewEvent.new event_data
90
+ @events_data << event
91
+ end
92
+ end
93
+ end
94
+
95
+ def render_template_hook
96
+ subscribe_asn 'render_template.action_view', ViewEvent
97
+ end
98
+
99
+ def render_partial_hook
100
+ subscribe_asn 'render_partial.action_view', ViewEvent
101
+ end
102
+
103
+ def active_job_hook
104
+ ::Vizsla::BackgroundJobInstrumentation.install :active_job
105
+ end
106
+
107
+ # ===---------------------------===
108
+ # DB Hooks
109
+ # ===---------------------------===
110
+
111
+ def postgres_hook
112
+ return unless defined? ::PG
113
+ ::Vizsla::Patches.patch_postgres do |event_data|
114
+ event = SQLEvent.new event_data
115
+ @events_data << event
116
+ end
117
+ end
118
+
119
+
120
+ def mysql2_hook
121
+ return unless defined? ::Mysql2
122
+ ::Vizsla::Patches.patch_mysql2 do |event_data|
123
+ event = SQLEvent.new event_data
124
+ @events_data << event
125
+ end
126
+ end
127
+
128
+ # ===---------------------------===
129
+ # Background Job Hooks
130
+ # ===---------------------------===
131
+
132
+ def sidekiq_hook
133
+ return unless defined? ::Sidekiq
134
+ ::Vizsla::BackgroundJobInstrumentation.install :sidekiq
135
+ end
136
+
137
+ def resque_hook
138
+ return unless defined? ::Resque
139
+ ::Vizsla::BackgroundJobInstrumentation.install :resque
140
+ end
141
+
142
+ # ===---------------------------===
143
+ # Sinatra Hooks
144
+ # ===---------------------------===
145
+
146
+ def sinatra_hook
147
+ ::Vizsla::Patches.patch_sinatra do |event_data|
148
+ event = SinatraEvent.new event_data
149
+ @events_data << event
150
+ end
151
+ end
152
+
153
+ # ===---------------------------===
154
+ # Aux
155
+ # ===---------------------------===
156
+
157
+ def subscribe_asn(event_name, event_klass)
158
+ ActiveSupport::Notifications.subscribe event_name do |*args|
159
+ event = event_klass.new args
160
+ @events_data << event if event.valid?
161
+ end
162
+ end
163
+
164
+ def rails_app?
165
+ defined? ::Rails
166
+ end
167
+
168
+ def sinatra_app?
169
+ defined? ::Sinatra
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,187 @@
1
+ module Vizsla
2
+ class SystemHealthSample
3
+ DATA_TYPE = 'system_health_sample'.freeze
4
+
5
+ def initialize(options = {})
6
+ @process = options[:process] || :web
7
+ @sampled_at = Time.new
8
+ @metrics = sample_metrics
9
+ end
10
+
11
+ def payload
12
+ {
13
+ type: DATA_TYPE,
14
+
15
+ data: {
16
+ sampled_at: @sampled_at,
17
+
18
+ metrics: @metrics
19
+ }
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def sample_metrics
26
+ {
27
+ process: @process.to_s,
28
+ cpu: processor_info,
29
+ memory: mem_info,
30
+ disks: disk_info,
31
+ machine_id: machine_info
32
+ }
33
+ end
34
+
35
+ def ruby_os_identifier
36
+ RbConfig::CONFIG['target_os']
37
+ end
38
+
39
+ def darwin?
40
+ !!(ruby_os_identifier =~ /darwin/i)
41
+ end
42
+
43
+ def linux?
44
+ !!(ruby_os_identifier =~ /linux/i)
45
+ end
46
+
47
+ def machine_info
48
+ ip_string = `dig +short myip.opendns.com @resolver1.opendns.com`.strip
49
+ hostname = `hostname`.strip
50
+
51
+ kernel = nil
52
+
53
+ if darwin?
54
+ kernel = 'darwin'
55
+ elsif linux?
56
+ kernel = 'linux'
57
+ end
58
+
59
+ {
60
+ hostname: hostname,
61
+ ip: ip_string,
62
+ kernel: kernel
63
+ }
64
+ end
65
+
66
+ def disk_info(disks = ["disk0"])
67
+ if darwin?
68
+ read_iostat(disks)
69
+ elsif linux?
70
+ parse_proc
71
+ end
72
+ end
73
+
74
+ def parse_proc
75
+ {
76
+ load_average: `cat /proc/loadavg | awk '{print $1,$2,$3}'`.strip
77
+ }
78
+ end
79
+
80
+ def read_iostat(disks)
81
+ disks_info = {}
82
+ captured_data = []
83
+
84
+ disks.each do |disk|
85
+ disk_data = `iostat -Ud #{disk}`.strip.split(/\n/)
86
+ captured_data << disk_data
87
+ disks_info["load_average"] = disk_data.last.strip.split(/\s+/)[3..-1].map(&:to_f) unless disks_info["load_average"]
88
+ end
89
+
90
+ captured_data.each_with_index do |disk, index|
91
+ disk_name = disks[index]
92
+ disk_stats = disk.last.strip.split(/\s+/)[0, 3]
93
+ disks_info[disk_name] = {
94
+ kb_per_trans: disk_stats[0].to_f,
95
+ trans_num: disk_stats[1].to_f,
96
+ mb_per_sec: disk_stats[2].to_f
97
+ }
98
+ end
99
+ disks_info
100
+ end
101
+
102
+ def mem_info
103
+ if darwin?
104
+ total, wired, free, used = get_mach_memory_stats
105
+ return {
106
+ total_memory: total,
107
+ wired_memory: wired,
108
+ free_memory: free,
109
+ used_memory: used
110
+ }
111
+ elsif linux?
112
+ total, cache, free, used, available = get_linux_memory_stats
113
+ return {
114
+ total_memory: total,
115
+ wired_memory: cache,
116
+ free_memory: free,
117
+ used_memory: used,
118
+ available_memory: available
119
+ }
120
+ end
121
+ end
122
+
123
+ def get_mach_memory_stats
124
+ used, wired, free = `top -l 1 -s 0 | grep PhysMem`.scan(/\d+/)
125
+ total = `sysctl -n hw.memsize`.to_i / 1024 / 1024
126
+ [total.to_i, wired.to_i, free.to_i, used.to_i]
127
+ end
128
+
129
+ def get_linux_memory_stats
130
+ total, used, free, _, cache, available = `free | grep Mem`.scan(/\d+/).map { |mem_stat| mem_stat.to_i / 1024 }
131
+ [total.to_s, cache.to_s, free.to_s, used.to_s, available.to_s]
132
+ end
133
+
134
+ def processor_info
135
+ info = {}
136
+
137
+ if darwin?
138
+ info[:model_name] = get_sysctl_value('machdep.cpu.brand_string')
139
+ info[:processor_count] = get_sysctl_value('hw.packages').to_i
140
+ info[:core_count] = get_sysctl_value('hw.physicalcpu_max').to_i,
141
+ info[:logical_cpu_count] = get_sysctl_value('hw.logicalcpu_max').to_i
142
+ elsif linux?
143
+ proc_string = read_proc('/proc/cpuinfo')
144
+ info = parse_proc_cpuinfo_string(proc_string)
145
+ end
146
+ info
147
+ end
148
+
149
+ def get_sysctl_value(key)
150
+ `sysctl -n #{key} 2>/dev/null`
151
+ end
152
+
153
+ def read_proc(path)
154
+ return nil unless File.exist? path
155
+ `cat #{path} 2>/dev/null`
156
+ end
157
+
158
+ def parse_proc_cpuinfo_string(proc_string)
159
+ threads = proc_string.split(/\n\n/).map { |core| core.split(/\n/) }
160
+
161
+ units = {}
162
+ cores = {}
163
+ model_name = nil
164
+
165
+ threads.each do |thread|
166
+ thread.each do |line|
167
+ if matched_line = line.match(/physical\ id\s*\:\s*(\d)/i)
168
+ id = matched_line[1]
169
+ units[id] = true
170
+ elsif matched_line = line.match(/core\ id\s*\:\s*(\d)/i)
171
+ id = matched_line[1]
172
+ cores[id] = true
173
+ elsif matched_line = line.match(/model\ name\s*\:\s*(.*)/i)
174
+ model_name = matched_line[1] unless model_name
175
+ end
176
+ end
177
+ end
178
+
179
+ {
180
+ model_name: model_name,
181
+ processor_count: units.count,
182
+ core_count: cores.count,
183
+ logical_cpu_count: threads.count
184
+ }
185
+ end
186
+ end
187
+ end