rails_performance 1.3.2 → 1.4.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
data/config/routes.rb CHANGED
@@ -14,6 +14,7 @@ RailsPerformance::Engine.routes.draw do
14
14
  get "/grape" => "rails_performance#grape", :as => :rails_performance_grape
15
15
  get "/rake" => "rails_performance#rake", :as => :rails_performance_rake
16
16
  get "/custom" => "rails_performance#custom", :as => :rails_performance_custom
17
+ get "/resources" => "rails_performance#resources", :as => :rails_performance_resources
17
18
  end
18
19
 
19
20
  Rails.application.routes.draw do
@@ -1,7 +1,7 @@
1
1
  if defined?(RailsPerformance)
2
2
  RailsPerformance.setup do |config|
3
3
  # Redis configuration
4
- config.redis = Redis::Namespace.new("#{Rails.env}-rails-performance", redis: Redis.new(url: ENV["REDIS_URL"].presence || "redis://127.0.0.1:6379/0"))
4
+ config.redis = Redis.new(url: ENV["REDIS_URL"].presence || "redis://127.0.0.1:6379/0")
5
5
 
6
6
  # All data we collect
7
7
  config.duration = 4.hours
@@ -36,7 +36,7 @@ if defined?(RailsPerformance)
36
36
 
37
37
  # You can ignore request paths by specifying the beginning of the path.
38
38
  # For example, all routes starting with '/admin' can be ignored:
39
- config.ignored_paths = ['/rails/performance']
39
+ config.ignored_paths = ["/rails/performance"]
40
40
 
41
41
  # store custom data for the request
42
42
  # config.custom_data_proc = proc do |env|
@@ -6,7 +6,8 @@ module RailsPerformance
6
6
  delayed_job: RailsPerformance::Models::DelayedJobRecord,
7
7
  grape: RailsPerformance::Models::GrapeRecord,
8
8
  rake: RailsPerformance::Models::RakeRecord,
9
- custom: RailsPerformance::Models::CustomRecord
9
+ custom: RailsPerformance::Models::CustomRecord,
10
+ resources: RailsPerformance::Models::ResourceRecord
10
11
  }
11
12
 
12
13
  attr_reader :q, :klass, :type
@@ -20,8 +21,9 @@ module RailsPerformance
20
21
 
21
22
  def db
22
23
  result = RailsPerformance::Models::Collection.new
23
- (0..(RailsPerformance::Utils.days + 1)).to_a.reverse_each do |e|
24
- RailsPerformance::DataSource.new(q: q.merge({on: (Time.current - e.days).to_date}), type: type).add_to(result)
24
+ now = Time.current
25
+ (0..(RailsPerformance::Utils.days)).to_a.reverse_each do |e|
26
+ RailsPerformance::DataSource.new(q: q.merge({on: (now - e.days).to_date}), type: type).add_to(result)
25
27
  end
26
28
  result
27
29
  end
@@ -53,6 +55,8 @@ module RailsPerformance
53
55
  case type
54
56
  when :requests
55
57
  "performance|*#{compile_requests_query}*|END|#{RailsPerformance::SCHEMA}"
58
+ when :resources
59
+ "resource|*#{compile_resource_query}*|END|#{RailsPerformance::SCHEMA}"
56
60
  when :sidekiq
57
61
  "sidekiq|*#{compile_sidekiq_query}*|END|#{RailsPerformance::SCHEMA}"
58
62
  when :delayed_job
@@ -89,6 +93,15 @@ module RailsPerformance
89
93
  str.join("*")
90
94
  end
91
95
 
96
+ def compile_resource_query
97
+ str = []
98
+ str << "server|#{q[:server]}|" if q[:server].present?
99
+ str << "context|#{q[:context]}|" if q[:context].present?
100
+ str << "role|#{q[:role]}|" if q[:role].present?
101
+ str << "datetime|#{q[:on].strftime("%Y%m%d")}*|" if q[:on].present?
102
+ str.join("*")
103
+ end
104
+
92
105
  def compile_delayed_job_query
93
106
  str = []
94
107
  str << "datetime|#{q[:on].strftime("%Y%m%d")}*|" if q[:on].present?
@@ -2,11 +2,23 @@ require "action_view/log_subscriber"
2
2
  require_relative "rails/middleware"
3
3
  require_relative "models/collection"
4
4
  require_relative "instrument/metrics_collector"
5
+ require_relative "extensions/resources_monitor"
5
6
 
6
7
  module RailsPerformance
7
8
  class Engine < ::Rails::Engine
8
9
  isolate_namespace RailsPerformance
9
10
 
11
+ initializer "rails_performance.resource_monitor" do
12
+ # check required gems are available
13
+ RailsPerformance._resource_monitor_enabled = !!(defined?(Sys::Filesystem) && defined?(Sys::CPU) && defined?(GetProcessMem))
14
+
15
+ next unless RailsPerformance.enabled
16
+ next if $rails_performance_running_mode == :console # rubocop:disable Style/GlobalVars
17
+
18
+ # start monitoring
19
+ RailsPerformance._resource_monitor = RailsPerformance::Extensions::ResourceMonitor.new("rails", "web")
20
+ end
21
+
10
22
  initializer "rails_performance.middleware" do |app|
11
23
  next unless RailsPerformance.enabled
12
24
 
@@ -24,10 +36,22 @@ module RailsPerformance
24
36
 
25
37
  if defined?(::Sidekiq)
26
38
  require_relative "gems/sidekiq_ext"
39
+
27
40
  Sidekiq.configure_server do |config|
28
41
  config.server_middleware do |chain|
29
42
  chain.add RailsPerformance::Gems::SidekiqExt
30
43
  end
44
+
45
+ config.on(:startup) do
46
+ if $rails_performance_running_mode != :console # rubocop:disable Style/GlobalVars
47
+ # stop web monitoring
48
+ # when we run sidekiq it also starts web monitoring (see above)
49
+ RailsPerformance._resource_monitor.stop_monitoring
50
+ RailsPerformance._resource_monitor = nil
51
+ # start background monitoring
52
+ RailsPerformance._resource_monitor = RailsPerformance::Extensions::ResourceMonitor.new("sidekiq", "background")
53
+ end
54
+ end
31
55
  end
32
56
  end
33
57
 
@@ -62,5 +86,9 @@ module RailsPerformance
62
86
  RailsPerformance::Gems::RakeExt.init
63
87
  end
64
88
  end
89
+
90
+ if defined?(::Rails::Console)
91
+ $rails_performance_running_mode = :console # rubocop:disable Style/GlobalVars
92
+ end
65
93
  end
66
94
  end
@@ -0,0 +1,103 @@
1
+ module RailsPerformance
2
+ module Extensions
3
+ class ResourceMonitor
4
+ attr_reader :context, :role
5
+
6
+ def initialize(context, role)
7
+ @context = context
8
+ @role = role
9
+ @mutex = Mutex.new
10
+ @thread = nil
11
+
12
+ return unless RailsPerformance._resource_monitor_enabled
13
+
14
+ start_monitoring
15
+ end
16
+
17
+ def start_monitoring
18
+ @mutex.synchronize do
19
+ return if @thread
20
+
21
+ # puts "Starting monitoring for #{context} - #{role}"
22
+ @thread = Thread.new do
23
+ loop do
24
+ run
25
+ rescue => e
26
+ ::Rails.logger.error "Monitor error: #{e.message}"
27
+ ensure
28
+ sleep 60
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def stop_monitoring
35
+ @mutex.synchronize do
36
+ return unless @thread
37
+
38
+ # puts "Stopping monitoring"
39
+ @thread.kill
40
+ @thread = nil
41
+ end
42
+ end
43
+
44
+ def run
45
+ cpu = fetch_process_cpu_usage
46
+ memory = fetch_process_memory_usage
47
+ disk = fetch_disk_usage
48
+
49
+ store_data({cpu:, memory:, disk:})
50
+ end
51
+
52
+ def fetch_process_cpu_usage
53
+ load_averages = Sys::CPU.load_avg
54
+ {
55
+ one_min: load_averages[0],
56
+ five_min: load_averages[1],
57
+ fifteen_min: load_averages[2]
58
+ }
59
+ rescue => e
60
+ ::Rails.logger.error "Error fetching CPU usage: #{e.message}"
61
+ {one_min: 0.0, five_min: 0.0, fifteen_min: 0.0}
62
+ end
63
+
64
+ def fetch_process_memory_usage
65
+ GetProcessMem.new.bytes
66
+ rescue => e
67
+ ::Rails.logger.error "Error fetching memory usage: #{e.message}"
68
+ 0
69
+ end
70
+
71
+ def fetch_disk_usage(path = "/")
72
+ stat = Sys::Filesystem.stat(path)
73
+ {
74
+ available: stat.blocks_available * stat.block_size,
75
+ total: stat.blocks * stat.block_size,
76
+ used: (stat.blocks - stat.blocks_available) * stat.block_size
77
+ }
78
+ rescue => e
79
+ ::Rails.logger.error "Error fetching disk space: #{e.message}"
80
+ {available: 0, total: 0, used: 0}
81
+ end
82
+
83
+ def store_data(data)
84
+ ::Rails.logger.info("Server: #{server_id}, Context: #{context}, Role: #{role}, data: #{data}")
85
+
86
+ now = Time.current
87
+ now = now.change(sec: 0, usec: 0)
88
+ RailsPerformance::Models::ResourceRecord.new(
89
+ server: server_id,
90
+ context: context,
91
+ role: role,
92
+ datetime: now.strftime(RailsPerformance::FORMAT),
93
+ datetimei: now.to_i,
94
+ json: data
95
+ ).save
96
+ end
97
+
98
+ def server_id
99
+ @server_id ||= ENV["SERVER_ID"] || `hostname`.strip
100
+ end
101
+ end
102
+ end
103
+ end
@@ -30,7 +30,11 @@ module RailsPerformance
30
30
  def self.from_db(key, value)
31
31
  items = key.split("|")
32
32
 
33
- parsed_value = JSON.parse(value) rescue {}
33
+ parsed_value = begin
34
+ JSON.parse(value)
35
+ rescue
36
+ {}
37
+ end
34
38
 
35
39
  RequestRecord.new(
36
40
  controller: items[2],
@@ -45,7 +49,7 @@ module RailsPerformance
45
49
  json: value,
46
50
  duration: parsed_value["duration"],
47
51
  view_runtime: parsed_value["view_runtime"],
48
- db_runtime: parsed_value["db_runtime"],
52
+ db_runtime: parsed_value["db_runtime"]
49
53
  )
50
54
  end
51
55
 
@@ -0,0 +1,47 @@
1
+ module RailsPerformance
2
+ module Models
3
+ class ResourceRecord < BaseRecord
4
+ attr_accessor :server, :context, :role, :datetime, :datetimei, :json
5
+
6
+ def initialize(server:, context:, role:, datetime:, datetimei:, json:)
7
+ @server = server
8
+ @context = context
9
+ @role = role
10
+ @datetime = datetime
11
+ @datetimei = datetimei
12
+ @json = json
13
+ end
14
+
15
+ def self.from_db(key, value)
16
+ items = key.split("|")
17
+
18
+ ResourceRecord.new(
19
+ server: items[2],
20
+ context: items[4],
21
+ role: items[6],
22
+ datetime: items[8],
23
+ datetimei: items[10],
24
+ json: value
25
+ )
26
+ end
27
+
28
+ def record_hash
29
+ {
30
+ server: server,
31
+ role: role,
32
+ context: context,
33
+ datetime: datetime,
34
+ datetimei: Time.at(datetimei.to_i),
35
+ cpu: value["cpu"],
36
+ memory: value["memory"],
37
+ disk: value["disk"]
38
+ }
39
+ end
40
+
41
+ def save
42
+ key = "resource|server|#{server}|context|#{context}|role|#{role}|datetime|#{datetime}|datetimei|#{datetimei}|END|#{RailsPerformance::SCHEMA}"
43
+ Utils.save_to_redis(key, json)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -32,6 +32,7 @@ module RailsPerformance
32
32
 
33
33
  def calculate_data
34
34
  now = Time.current
35
+ now = now.change(sec: 0, usec: 0)
35
36
  stop = Time.at(60 * (now.to_i / 60))
36
37
  offset = RailsPerformance::Reports::BaseReport.time_in_app_time_zone(now).utc_offset
37
38
  current = stop - RailsPerformance.duration
@@ -39,7 +40,6 @@ module RailsPerformance
39
40
  @data = []
40
41
  all = {}
41
42
 
42
- # read current values
43
43
  db.group_by(group).each do |(k, v)|
44
44
  yield(all, k, v)
45
45
  end
@@ -48,13 +48,46 @@ module RailsPerformance
48
48
  while current <= stop
49
49
  key = current.strftime(RailsPerformance::FORMAT)
50
50
  views = all[key].presence || 0
51
- @data << [(current.to_i + offset) * 1000, views.round(2)]
51
+ @data << [(current.to_i + offset) * 1000, views.is_a?(Numeric) ? views.round(2) : views]
52
52
  current += 1.minute
53
53
  end
54
54
 
55
55
  # sort by time
56
56
  @data.sort!
57
57
  end
58
+
59
+ # Generate series for our time range -> (now - duration)..now
60
+ # {
61
+ # 1732125540000 => 0,
62
+ # 1732125550000 => 0,
63
+ # ....
64
+ # }
65
+ def nil_data
66
+ @nil_data ||= begin
67
+ result = {}
68
+ now = Time.current
69
+ now = now.change(sec: 0, usec: 0)
70
+ stop = Time.at(60 * (now.to_i / 60))
71
+ offset = RailsPerformance::Reports::BaseReport.time_in_app_time_zone(now).utc_offset
72
+ current = stop - RailsPerformance.duration
73
+
74
+ while current <= stop
75
+ current.strftime(RailsPerformance::FORMAT)
76
+ result[(current.to_i + offset) * 1000] = nil
77
+ current += 1.minute
78
+ end
79
+
80
+ result
81
+ end
82
+ end
83
+
84
+ # {
85
+ # 1732125540000 => 1,
86
+ # 1732125550000 => 0,
87
+ # }
88
+ def nullify_data(input)
89
+ nil_data.merge(input).sort
90
+ end
58
91
  end
59
92
  end
60
93
  end
@@ -2,7 +2,7 @@ module RailsPerformance
2
2
  module Reports
3
3
  class PercentileReport < BaseReport
4
4
  def data
5
- durations = db.data.collect(&:duration)
5
+ durations = db.data.collect(&:duration).compact
6
6
  {
7
7
  p50: RailsPerformance::Utils.percentile(durations, 50),
8
8
  p95: RailsPerformance::Utils.percentile(durations, 95),
@@ -21,7 +21,7 @@ module RailsPerformance
21
21
  db_runtime_slowest: db_runtimes.max,
22
22
  p50_duration: RailsPerformance::Utils.percentile(durations, 50),
23
23
  p95_duration: RailsPerformance::Utils.percentile(durations, 95),
24
- p99_duration: RailsPerformance::Utils.percentile(durations, 99),
24
+ p99_duration: RailsPerformance::Utils.percentile(durations, 99)
25
25
  }
26
26
  end.sort_by { |e| -e[sort].to_f } # to_f because could ne NaN or nil
27
27
  end
@@ -0,0 +1,44 @@
1
+ module RailsPerformance
2
+ module Reports
3
+ class ResourcesReport < BaseReport
4
+ def self.x
5
+ @datasource = RailsPerformance::DataSource.new(type: :resources)
6
+ db = @datasource.db
7
+ @data = RailsPerformance::Reports::ResourcesReport.new(db)
8
+ # RailsPerformance::Reports::ResourcesReport.x
9
+ end
10
+
11
+ def data
12
+ @data ||= db.data
13
+ .collect { |e| e.record_hash }
14
+ .group_by { |e| e[:server] + "///" + e[:context] + "///" + e[:role] }
15
+ .transform_values { |v| v.sort { |a, b| b[sort] <=> a[sort] } }
16
+ .transform_values { |v| v.map { |e| e.merge({datetimei: e[:datetimei].to_i}) } }
17
+ end
18
+
19
+ def cpu
20
+ @cpu ||= data.transform_values do |v|
21
+ nullify_data(v.each_with_object({}) do |e, res|
22
+ res[e[:datetimei] * 1000] = e[:cpu]["one_min"].to_f.round(2)
23
+ end)
24
+ end
25
+ end
26
+
27
+ def memory
28
+ @memory ||= data.transform_values do |v|
29
+ nullify_data(v.each_with_object({}) do |e, res|
30
+ res[e[:datetimei] * 1000] = e[:memory].to_f.round(2)
31
+ end)
32
+ end
33
+ end
34
+
35
+ def disk
36
+ @disk ||= data.transform_values do |v|
37
+ nullify_data(v.each_with_object({}) do |e, res|
38
+ res[e[:datetimei] * 1000] = e[:disk]["available"].to_f.round(2)
39
+ end)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,4 +1,4 @@
1
1
  module RailsPerformance
2
- VERSION = "1.3.2"
2
+ VERSION = "1.4.0.alpha1"
3
3
  SCHEMA = "1.0.1"
4
4
  end
@@ -11,6 +11,7 @@ require_relative "rails_performance/models/delayed_job_record"
11
11
  require_relative "rails_performance/models/grape_record"
12
12
  require_relative "rails_performance/models/trace_record"
13
13
  require_relative "rails_performance/models/rake_record"
14
+ require_relative "rails_performance/models/resource_record"
14
15
  require_relative "rails_performance/models/custom_record"
15
16
  require_relative "rails_performance/data_source"
16
17
  require_relative "rails_performance/utils"
@@ -24,6 +25,7 @@ require_relative "rails_performance/reports/slow_requests_report"
24
25
  require_relative "rails_performance/reports/breakdown_report"
25
26
  require_relative "rails_performance/reports/trace_report"
26
27
  require_relative "rails_performance/reports/percentile_report"
28
+ require_relative "rails_performance/reports/resources_report"
27
29
  require_relative "rails_performance/extensions/trace"
28
30
  require_relative "rails_performance/thread/current_request"
29
31
 
@@ -117,6 +119,17 @@ module RailsPerformance
117
119
  mattr_accessor :ignore_trace_headers
118
120
  @@ignore_trace_headers = ["datetimei"]
119
121
 
122
+ mattr_accessor :_resource_monitor
123
+ @@_resource_monitor = nil
124
+
125
+ # to check if we are running in console mode
126
+ mattr_accessor :_running_mode
127
+ @@_running_mode = nil
128
+
129
+ # by default we don't want to monitor resources, but we can enable it by adding required gems
130
+ mattr_accessor :_resource_monitor_enabled
131
+ @@_resource_monitor_enabled = false
132
+
120
133
  def self.setup
121
134
  yield(self)
122
135
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_performance
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.0.alpha1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Kasyanchuk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-08 00:00:00.000000000 Z
11
+ date: 2024-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -248,6 +248,48 @@ dependencies:
248
248
  - - ">="
249
249
  - !ruby/object:Gem::Version
250
250
  version: '0'
251
+ - !ruby/object:Gem::Dependency
252
+ name: sys-filesystem
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0'
258
+ type: :development
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0'
265
+ - !ruby/object:Gem::Dependency
266
+ name: sys-cpu
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - ">="
270
+ - !ruby/object:Gem::Version
271
+ version: '0'
272
+ type: :development
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - ">="
277
+ - !ruby/object:Gem::Version
278
+ version: '0'
279
+ - !ruby/object:Gem::Dependency
280
+ name: get_process_mem
281
+ requirement: !ruby/object:Gem::Requirement
282
+ requirements:
283
+ - - ">="
284
+ - !ruby/object:Gem::Version
285
+ version: '0'
286
+ type: :development
287
+ prerelease: false
288
+ version_requirements: !ruby/object:Gem::Requirement
289
+ requirements:
290
+ - - ">="
291
+ - !ruby/object:Gem::Version
292
+ version: '0'
251
293
  description: 3rd party dependency-free solution how to monitor performance of your
252
294
  Rails applications.
253
295
  email:
@@ -299,6 +341,7 @@ files:
299
341
  - app/views/rails_performance/rails_performance/recent.html.erb
300
342
  - app/views/rails_performance/rails_performance/recent.js.erb
301
343
  - app/views/rails_performance/rails_performance/requests.html.erb
344
+ - app/views/rails_performance/rails_performance/resources.html.erb
302
345
  - app/views/rails_performance/rails_performance/sidekiq.html.erb
303
346
  - app/views/rails_performance/rails_performance/slow.html.erb
304
347
  - app/views/rails_performance/rails_performance/summary.js.erb
@@ -316,6 +359,7 @@ files:
316
359
  - lib/rails_performance.rb
317
360
  - lib/rails_performance/data_source.rb
318
361
  - lib/rails_performance/engine.rb
362
+ - lib/rails_performance/extensions/resources_monitor.rb
319
363
  - lib/rails_performance/extensions/trace.rb
320
364
  - lib/rails_performance/gems/custom_ext.rb
321
365
  - lib/rails_performance/gems/delayed_job_ext.rb
@@ -330,6 +374,7 @@ files:
330
374
  - lib/rails_performance/models/grape_record.rb
331
375
  - lib/rails_performance/models/rake_record.rb
332
376
  - lib/rails_performance/models/request_record.rb
377
+ - lib/rails_performance/models/resource_record.rb
333
378
  - lib/rails_performance/models/sidekiq_record.rb
334
379
  - lib/rails_performance/models/trace_record.rb
335
380
  - lib/rails_performance/rails/middleware.rb
@@ -340,6 +385,7 @@ files:
340
385
  - lib/rails_performance/reports/percentile_report.rb
341
386
  - lib/rails_performance/reports/recent_requests_report.rb
342
387
  - lib/rails_performance/reports/requests_report.rb
388
+ - lib/rails_performance/reports/resources_report.rb
343
389
  - lib/rails_performance/reports/response_time_report.rb
344
390
  - lib/rails_performance/reports/slow_requests_report.rb
345
391
  - lib/rails_performance/reports/throughput_report.rb
@@ -366,7 +412,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
366
412
  - !ruby/object:Gem::Version
367
413
  version: '0'
368
414
  requirements: []
369
- rubygems_version: 3.3.7
415
+ rubygems_version: 3.5.22
370
416
  signing_key:
371
417
  specification_version: 4
372
418
  summary: Simple Rails Performance tracker. Alternative to the NewRelic, Datadog or