railswatch 1.0.0
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +485 -0
- data/Rakefile +37 -0
- data/app/assets/config/railswatch_manifest.js +0 -0
- data/app/assets/images/activity.svg +13 -0
- data/app/assets/images/bot.svg +1 -0
- data/app/assets/images/close.svg +13 -0
- data/app/assets/images/details.svg +3 -0
- data/app/assets/images/download.svg +3 -0
- data/app/assets/images/export.svg +13 -0
- data/app/assets/images/external.svg +1 -0
- data/app/assets/images/git.svg +1 -0
- data/app/assets/images/github.svg +1 -0
- data/app/assets/images/home.svg +16 -0
- data/app/assets/images/import.svg +13 -0
- data/app/assets/images/menu.svg +16 -0
- data/app/assets/images/moon.svg +3 -0
- data/app/assets/images/stat.svg +1 -0
- data/app/assets/images/sun.svg +4 -0
- data/app/assets/images/user.svg +1 -0
- data/app/controllers/railswatch/base_controller.rb +35 -0
- data/app/controllers/railswatch/concerns/csv_exportable.rb +31 -0
- data/app/controllers/railswatch/railswatch_controller.rb +183 -0
- data/app/engine_assets/javascripts/apex_ext.js +30 -0
- data/app/engine_assets/javascripts/application.js +9 -0
- data/app/engine_assets/javascripts/autoupdate.js +79 -0
- data/app/engine_assets/javascripts/charts.js +279 -0
- data/app/engine_assets/javascripts/navbar.js +11 -0
- data/app/engine_assets/javascripts/panel.js +43 -0
- data/app/engine_assets/javascripts/table.js +12 -0
- data/app/engine_assets/javascripts/theme.js +43 -0
- data/app/engine_assets/stylesheets/panel.css +111 -0
- data/app/engine_assets/stylesheets/responsive.css +102 -0
- data/app/engine_assets/stylesheets/style.css +960 -0
- data/app/helpers/railswatch/railswatch_helper.rb +338 -0
- data/app/views/railswatch/_panel.html.erb +15 -0
- data/app/views/railswatch/layouts/railswatch.html.erb +81 -0
- data/app/views/railswatch/railswatch/_card.html.erb +7 -0
- data/app/views/railswatch/railswatch/_chart.html.erb +13 -0
- data/app/views/railswatch/railswatch/_crashes_table_content.html.erb +62 -0
- data/app/views/railswatch/railswatch/_custom_events_table_content.html.erb +27 -0
- data/app/views/railswatch/railswatch/_delayed_job_table_content.html.erb +52 -0
- data/app/views/railswatch/railswatch/_export.html.erb +4 -0
- data/app/views/railswatch/railswatch/_grape_requests_table_content.html.erb +31 -0
- data/app/views/railswatch/railswatch/_overview.html.erb +124 -0
- data/app/views/railswatch/railswatch/_rake_tasks_table_content.html.erb +25 -0
- data/app/views/railswatch/railswatch/_recent_requests_table_content.html.erb +28 -0
- data/app/views/railswatch/railswatch/_recent_row.html.erb +41 -0
- data/app/views/railswatch/railswatch/_requests_table_content.html.erb +51 -0
- data/app/views/railswatch/railswatch/_sidekiq_jobs_table_content.html.erb +50 -0
- data/app/views/railswatch/railswatch/_summary.html.erb +50 -0
- data/app/views/railswatch/railswatch/_table.html.erb +30 -0
- data/app/views/railswatch/railswatch/_trace.html.erb +78 -0
- data/app/views/railswatch/railswatch/crashes.html.erb +2 -0
- data/app/views/railswatch/railswatch/custom.html.erb +6 -0
- data/app/views/railswatch/railswatch/delayed_job.html.erb +6 -0
- data/app/views/railswatch/railswatch/grape.html.erb +6 -0
- data/app/views/railswatch/railswatch/index.html.erb +9 -0
- data/app/views/railswatch/railswatch/rake.html.erb +6 -0
- data/app/views/railswatch/railswatch/recent.html.erb +2 -0
- data/app/views/railswatch/railswatch/requests.html.erb +2 -0
- data/app/views/railswatch/railswatch/resources.html.erb +28 -0
- data/app/views/railswatch/railswatch/sidekiq.html.erb +6 -0
- data/app/views/railswatch/railswatch/slow.html.erb +2 -0
- data/app/views/railswatch/railswatch/summary.js.erb +3 -0
- data/app/views/railswatch/railswatch/trace.js.erb +9 -0
- data/app/views/railswatch/shared/_header.html.erb +39 -0
- data/app/views/railswatch/shared/_page_header.html.erb +23 -0
- data/config/routes.rb +27 -0
- data/lib/generators/railswatch/install/USAGE +19 -0
- data/lib/generators/railswatch/install/install_generator.rb +46 -0
- data/lib/generators/railswatch/install/templates/create_railswatch_tables.rb +140 -0
- data/lib/generators/railswatch/install/templates/initializer.rb +87 -0
- data/lib/railswatch/data_source.rb +106 -0
- data/lib/railswatch/engine.rb +103 -0
- data/lib/railswatch/events/record.rb +63 -0
- data/lib/railswatch/extensions/trace.rb +14 -0
- data/lib/railswatch/extensions/trace_db.rb +21 -0
- data/lib/railswatch/gems/custom_ext.rb +38 -0
- data/lib/railswatch/gems/delayed_job_ext.rb +70 -0
- data/lib/railswatch/gems/grape_ext.rb +64 -0
- data/lib/railswatch/gems/rake_ext.rb +69 -0
- data/lib/railswatch/gems/sidekiq_ext.rb +55 -0
- data/lib/railswatch/instrument/metrics_collector.rb +70 -0
- data/lib/railswatch/interface.rb +9 -0
- data/lib/railswatch/models/application_record.rb +31 -0
- data/lib/railswatch/models/base_record.rb +59 -0
- data/lib/railswatch/models/collection.rb +37 -0
- data/lib/railswatch/models/custom_record.rb +32 -0
- data/lib/railswatch/models/delayed_job_record.rb +39 -0
- data/lib/railswatch/models/event_record.rb +11 -0
- data/lib/railswatch/models/grape_record.rb +61 -0
- data/lib/railswatch/models/rake_record.rb +33 -0
- data/lib/railswatch/models/request_record.rb +105 -0
- data/lib/railswatch/models/resource_record.rb +33 -0
- data/lib/railswatch/models/sidekiq_record.rb +41 -0
- data/lib/railswatch/models/trace_record.rb +21 -0
- data/lib/railswatch/pruner.rb +47 -0
- data/lib/railswatch/rails/middleware.rb +117 -0
- data/lib/railswatch/rails/query_builder.rb +20 -0
- data/lib/railswatch/reports/annotations_report.rb +13 -0
- data/lib/railswatch/reports/base_report.rb +48 -0
- data/lib/railswatch/reports/breakdown_report.rb +11 -0
- data/lib/railswatch/reports/crash_report.rb +11 -0
- data/lib/railswatch/reports/overview_report.rb +88 -0
- data/lib/railswatch/reports/percentile_report.rb +16 -0
- data/lib/railswatch/reports/recent_requests_report.rb +21 -0
- data/lib/railswatch/reports/requests_report.rb +75 -0
- data/lib/railswatch/reports/resources_report.rb +42 -0
- data/lib/railswatch/reports/response_time_report.rb +27 -0
- data/lib/railswatch/reports/slow_requests_report.rb +21 -0
- data/lib/railswatch/reports/throughput_report.rb +16 -0
- data/lib/railswatch/reports/trace_report.rb +17 -0
- data/lib/railswatch/system_monitor/resources_monitor.rb +88 -0
- data/lib/railswatch/thread/current_request.rb +37 -0
- data/lib/railswatch/utils.rb +58 -0
- data/lib/railswatch/version.rb +7 -0
- data/lib/railswatch/widgets/base.rb +17 -0
- data/lib/railswatch/widgets/card.rb +19 -0
- data/lib/railswatch/widgets/chart.rb +33 -0
- data/lib/railswatch/widgets/crashes_table.rb +27 -0
- data/lib/railswatch/widgets/custom_events_table.rb +48 -0
- data/lib/railswatch/widgets/delayed_job_table.rb +31 -0
- data/lib/railswatch/widgets/grape_requests_table.rb +31 -0
- data/lib/railswatch/widgets/percentile_card.rb +23 -0
- data/lib/railswatch/widgets/rake_tasks_table.rb +31 -0
- data/lib/railswatch/widgets/recent_requests_table.rb +35 -0
- data/lib/railswatch/widgets/requests_table.rb +27 -0
- data/lib/railswatch/widgets/resource_chart.rb +116 -0
- data/lib/railswatch/widgets/response_time_chart.rb +29 -0
- data/lib/railswatch/widgets/sidekiq_jobs_table.rb +31 -0
- data/lib/railswatch/widgets/slow_requests_table.rb +33 -0
- data/lib/railswatch/widgets/table.rb +43 -0
- data/lib/railswatch/widgets/throughput_chart.rb +29 -0
- data/lib/railswatch.rb +184 -0
- data/lib/tasks/railswatch.rake +9 -0
- metadata +445 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class RequestRecord < BaseRecord
|
|
6
|
+
self.table_name = 'railswatch_request_records'
|
|
7
|
+
|
|
8
|
+
serialize :custom_data, coder: JSON
|
|
9
|
+
serialize :backtrace, coder: JSON
|
|
10
|
+
serialize :request_context, coder: JSON
|
|
11
|
+
|
|
12
|
+
attr_accessor :exception_object
|
|
13
|
+
|
|
14
|
+
validates :request_id, presence: true
|
|
15
|
+
validates :occurred_at, presence: true
|
|
16
|
+
|
|
17
|
+
def method
|
|
18
|
+
http_method
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def method=(value)
|
|
22
|
+
self.http_method = value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def view_runtime
|
|
26
|
+
view_runtime_ms
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def view_runtime=(value)
|
|
30
|
+
self.view_runtime_ms = value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def db_runtime
|
|
34
|
+
db_runtime_ms
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def db_runtime=(value)
|
|
38
|
+
self.db_runtime_ms = value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def duration
|
|
42
|
+
duration_ms
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def duration=(value)
|
|
46
|
+
self.duration_ms = value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def controller_action
|
|
50
|
+
"#{controller}##{action}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def controller_action_format
|
|
54
|
+
"#{controller}##{action}|#{format}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def payload_hash
|
|
58
|
+
{
|
|
59
|
+
'view_runtime' => view_runtime_ms,
|
|
60
|
+
'db_runtime' => db_runtime_ms,
|
|
61
|
+
'duration' => duration_ms,
|
|
62
|
+
'http_referer' => http_referer,
|
|
63
|
+
'custom_data' => custom_data,
|
|
64
|
+
'exception' => exception,
|
|
65
|
+
'backtrace' => backtrace,
|
|
66
|
+
'request_context' => request_context
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def record_hash
|
|
71
|
+
base_record_hash.merge(custom_record_hash)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def base_record_hash # rubocop:disable Metrics/MethodLength
|
|
75
|
+
{
|
|
76
|
+
controller: controller,
|
|
77
|
+
action: action,
|
|
78
|
+
format: self.format,
|
|
79
|
+
status: status,
|
|
80
|
+
method: http_method,
|
|
81
|
+
path: path,
|
|
82
|
+
request_id: request_id,
|
|
83
|
+
datetime: occurred_at,
|
|
84
|
+
datetimei: occurred_at.to_i,
|
|
85
|
+
duration: duration_ms,
|
|
86
|
+
db_runtime: db_runtime_ms,
|
|
87
|
+
view_runtime: view_runtime_ms,
|
|
88
|
+
exception: exception,
|
|
89
|
+
backtrace: backtrace,
|
|
90
|
+
http_referer: http_referer,
|
|
91
|
+
request_context: request_context
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def custom_record_hash
|
|
96
|
+
custom_data.is_a?(Hash) ? custom_data.deep_symbolize_keys : {}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
before_save do
|
|
100
|
+
self.exception = Array.wrap(exception).compact.join(' ') if exception.is_a?(Array)
|
|
101
|
+
self.backtrace = exception_object.backtrace.take(20) if exception_object
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class ResourceRecord < BaseRecord
|
|
6
|
+
self.table_name = 'railswatch_resource_records'
|
|
7
|
+
|
|
8
|
+
serialize :payload, coder: JSON
|
|
9
|
+
|
|
10
|
+
def json
|
|
11
|
+
payload
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def json=(value)
|
|
15
|
+
self.payload = value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def payload_hash
|
|
19
|
+
payload || {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record_hash
|
|
23
|
+
payload_hash.symbolize_keys.merge(
|
|
24
|
+
server: server,
|
|
25
|
+
role: role,
|
|
26
|
+
context: context,
|
|
27
|
+
datetime: occurred_at,
|
|
28
|
+
datetimei: occurred_at.to_i
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class SidekiqRecord < BaseRecord
|
|
6
|
+
self.table_name = 'railswatch_sidekiq_records'
|
|
7
|
+
|
|
8
|
+
serialize :job_args, coder: JSON
|
|
9
|
+
|
|
10
|
+
def duration
|
|
11
|
+
duration_ms
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def duration=(value)
|
|
15
|
+
self.duration_ms = value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def payload_hash
|
|
19
|
+
{
|
|
20
|
+
'message' => message,
|
|
21
|
+
'duration' => duration_ms
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def record_hash # rubocop:disable Metrics/MethodLength
|
|
26
|
+
{
|
|
27
|
+
worker: worker,
|
|
28
|
+
queue: queue,
|
|
29
|
+
jid: jid,
|
|
30
|
+
status: status,
|
|
31
|
+
datetimei: occurred_at.to_i,
|
|
32
|
+
datetime: Railswatch::Utils.from_datetimei(start_timei.to_i),
|
|
33
|
+
duration: duration_ms,
|
|
34
|
+
message: message,
|
|
35
|
+
job_args: job_args,
|
|
36
|
+
error_backtrace: error_backtrace
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class TraceRecord < BaseRecord
|
|
6
|
+
self.table_name = 'railswatch_trace_records'
|
|
7
|
+
|
|
8
|
+
serialize :entries, coder: JSON
|
|
9
|
+
|
|
10
|
+
validates :request_id, presence: true
|
|
11
|
+
|
|
12
|
+
def value
|
|
13
|
+
entries || []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def value=(val)
|
|
17
|
+
self.entries = val
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
class Pruner
|
|
5
|
+
DEFAULT_BATCH_SIZE = 1_000
|
|
6
|
+
|
|
7
|
+
MODEL_TYPES = {
|
|
8
|
+
requests: 'RequestRecord',
|
|
9
|
+
sidekiq: 'SidekiqRecord',
|
|
10
|
+
delayed_job: 'DelayedJobRecord',
|
|
11
|
+
grape: 'GrapeRecord',
|
|
12
|
+
rake: 'RakeRecord',
|
|
13
|
+
custom: 'CustomRecord',
|
|
14
|
+
resources: 'ResourceRecord',
|
|
15
|
+
traces: 'TraceRecord',
|
|
16
|
+
events: 'EventRecord'
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def self.call(batch_size: DEFAULT_BATCH_SIZE)
|
|
20
|
+
MODEL_TYPES.each_with_object({}) do |(type, class_name), results|
|
|
21
|
+
deleted = deleted_records(type, class_name, batch_size)
|
|
22
|
+
results[type] = deleted unless deleted.nil?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.deleted_records(type, class_name, batch_size)
|
|
27
|
+
retention = Railswatch.retention[type]
|
|
28
|
+
return if retention.nil?
|
|
29
|
+
|
|
30
|
+
model = Railswatch::Models.const_get(class_name)
|
|
31
|
+
threshold = Railswatch::Utils.kind_of_now - retention
|
|
32
|
+
|
|
33
|
+
delete_in_batches(model, threshold, batch_size)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.delete_in_batches(model, threshold, batch_size)
|
|
37
|
+
deleted = 0
|
|
38
|
+
|
|
39
|
+
loop do
|
|
40
|
+
ids = model.where('occurred_at < ?', threshold).limit(batch_size).pluck(:id)
|
|
41
|
+
break deleted if ids.empty?
|
|
42
|
+
|
|
43
|
+
deleted += model.where(id: ids).delete_all
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Rails
|
|
5
|
+
class MiddlewareTraceStorerAndCleanup
|
|
6
|
+
def initialize(app)
|
|
7
|
+
@app = app
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(env)
|
|
11
|
+
dup.call!(env)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call!(env)
|
|
15
|
+
Railswatch.skip = true if monitoring_path?(env)
|
|
16
|
+
|
|
17
|
+
@status, @headers, @response = @app.call(env)
|
|
18
|
+
save_trace_record unless Railswatch.skip
|
|
19
|
+
|
|
20
|
+
CurrentRequest.cleanup
|
|
21
|
+
|
|
22
|
+
[@status, @headers, @response]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def monitoring_path?(env)
|
|
28
|
+
/#{Railswatch.mount_at}/.match?(env['PATH_INFO'])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save_trace_record
|
|
32
|
+
Railswatch::Models::TraceRecord.new(
|
|
33
|
+
request_id: CurrentRequest.current.request_id,
|
|
34
|
+
value: CurrentRequest.current.tracings,
|
|
35
|
+
occurred_at: Railswatch::Utils.time
|
|
36
|
+
).save
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class Middleware
|
|
41
|
+
def initialize(app)
|
|
42
|
+
@app = app
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def call(env)
|
|
46
|
+
dup.call!(env)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call!(env)
|
|
50
|
+
@status, @headers, @response = @app.call(env)
|
|
51
|
+
save_request_record(env)
|
|
52
|
+
|
|
53
|
+
[@status, @headers, @response]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def save_request_record(env)
|
|
59
|
+
data = CurrentRequest.current.data
|
|
60
|
+
return if Railswatch.skip || CurrentRequest.current.ignore.include?(:monitoring) || data.blank?
|
|
61
|
+
|
|
62
|
+
record = Railswatch::Models::RequestRecord.new(**data, request_id: CurrentRequest.current.request_id)
|
|
63
|
+
normalize_status!(record)
|
|
64
|
+
add_http_referer!(record, env)
|
|
65
|
+
add_custom_data!(record, env)
|
|
66
|
+
add_request_context!(record, env)
|
|
67
|
+
record.save
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def normalize_status!(record)
|
|
71
|
+
record.status ||= @status
|
|
72
|
+
record.status = record.status.to_s if record.status.present?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_http_referer!(record, env)
|
|
76
|
+
record.http_referer = env['HTTP_REFERER'] if record.status == '404'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_custom_data!(record, env)
|
|
80
|
+
return unless Railswatch.custom_data_proc
|
|
81
|
+
|
|
82
|
+
record.custom_data = Railswatch.custom_data_proc.call(env)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def add_request_context!(record, env)
|
|
86
|
+
record.request_context = {
|
|
87
|
+
ip: request_ip(env),
|
|
88
|
+
user_agent: env['HTTP_USER_AGENT'],
|
|
89
|
+
params: filtered_request_params(env).presence,
|
|
90
|
+
user: current_user_info(env)
|
|
91
|
+
}.compact
|
|
92
|
+
rescue StandardError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def request_ip(env)
|
|
97
|
+
forwarded = env['HTTP_X_FORWARDED_FOR']
|
|
98
|
+
forwarded ? forwarded.split(',').first&.strip : env['REMOTE_ADDR']
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def filtered_request_params(env)
|
|
102
|
+
params = ActionDispatch::Request.new(env).params.to_h
|
|
103
|
+
Railswatch::Utils.filter_params(params)
|
|
104
|
+
rescue StandardError
|
|
105
|
+
{}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def current_user_info(env)
|
|
109
|
+
return nil unless Railswatch.current_user_proc
|
|
110
|
+
|
|
111
|
+
Railswatch.current_user_proc.call(env)&.presence
|
|
112
|
+
rescue StandardError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Rails
|
|
5
|
+
class QueryBuilder
|
|
6
|
+
def self.compose_from(params)
|
|
7
|
+
result = {}
|
|
8
|
+
|
|
9
|
+
result[:controller] = params[:controller_eq]
|
|
10
|
+
result[:action] = params[:action_eq]
|
|
11
|
+
result[:format] = params[:format_eq]
|
|
12
|
+
result[:status] = params[:status_eq]
|
|
13
|
+
|
|
14
|
+
result.delete_if { |_k, v| v.nil? }
|
|
15
|
+
|
|
16
|
+
{ query: result }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Reports
|
|
5
|
+
class BaseReport
|
|
6
|
+
attr_reader :db, :group, :sort, :title
|
|
7
|
+
|
|
8
|
+
def initialize(db, group: nil, sort: nil, title: nil)
|
|
9
|
+
@db = db
|
|
10
|
+
@group = group
|
|
11
|
+
@sort = sort
|
|
12
|
+
@title = title
|
|
13
|
+
set_defaults
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def set_defaults; end
|
|
17
|
+
|
|
18
|
+
def self.time_in_app_time_zone(time)
|
|
19
|
+
app_time_zone = ::Rails.application.config.time_zone
|
|
20
|
+
app_time_zone.present? ? time.in_time_zone(app_time_zone) : time
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def nil_data(duration = Railswatch.duration)
|
|
24
|
+
@nil_data_cache ||= {}
|
|
25
|
+
@nil_data_cache[duration] ||= build_nil_data(duration)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def nullify_data(input, duration = Railswatch.duration)
|
|
29
|
+
nil_data(duration).merge(input).sort
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_nil_data(duration)
|
|
35
|
+
result = {}
|
|
36
|
+
stop = Railswatch::Utils.kind_of_now.change(sec: 0, usec: 0)
|
|
37
|
+
current = stop - duration
|
|
38
|
+
|
|
39
|
+
while current <= stop
|
|
40
|
+
result[current.to_i * 1000] = nil
|
|
41
|
+
current += 1.minute
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Reports
|
|
5
|
+
class OverviewReport < BaseReport
|
|
6
|
+
def data
|
|
7
|
+
rows = request_rows
|
|
8
|
+
grouped = grouped_rows(rows)
|
|
9
|
+
build_overview_data(rows, grouped)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def build_overview_data(rows, grouped)
|
|
15
|
+
{
|
|
16
|
+
total_requests: rows.size,
|
|
17
|
+
average_duration: average_duration(rows),
|
|
18
|
+
error_rate: error_rate(rows),
|
|
19
|
+
slow_requests: slow_request_count(rows),
|
|
20
|
+
unique_endpoints: grouped.keys.size,
|
|
21
|
+
busiest_endpoint: endpoint_volume(grouped),
|
|
22
|
+
slowest_endpoint: slowest_endpoint(grouped),
|
|
23
|
+
latest_request_at: latest_request_at(rows)
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def request_rows
|
|
28
|
+
db.pluck(:controller, :action, :status, :duration_ms, :occurred_at)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def grouped_rows(rows)
|
|
32
|
+
rows.group_by { |controller, action, *_rest| "#{controller}##{action}" }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def average_duration(rows)
|
|
36
|
+
return nil if rows.empty?
|
|
37
|
+
|
|
38
|
+
rows.sum { |row| duration_value(row) } / rows.size
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def error_rate(rows)
|
|
42
|
+
return 0 if rows.empty?
|
|
43
|
+
|
|
44
|
+
(error_count(rows).to_f / rows.size) * 100
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def error_count(rows)
|
|
48
|
+
rows.count { |row| row[2].to_s.start_with?('5') }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def slow_request_count(rows)
|
|
52
|
+
rows.count { |row| duration_value(row) >= slow_threshold }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def slow_threshold
|
|
56
|
+
Railswatch.slow_requests_threshold.to_f
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def endpoint_volume(grouped)
|
|
60
|
+
name, samples = grouped.max_by { |_group_name, group_rows| group_rows.size }
|
|
61
|
+
{ name: name, count: samples&.size.to_i }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def slowest_endpoint(grouped)
|
|
65
|
+
name, samples = grouped.max_by do |_group_name, group_rows|
|
|
66
|
+
endpoint_p95(group_rows).to_f
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
name: name,
|
|
71
|
+
p95_duration: samples.present? ? endpoint_p95(samples) : nil
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def endpoint_p95(rows)
|
|
76
|
+
Railswatch::Utils.percentile(rows.filter_map { |row| row[3] }, 95)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def latest_request_at(rows)
|
|
80
|
+
rows.max_by { |row| row[4].to_i }&.last
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def duration_value(row)
|
|
84
|
+
row[3].to_f
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Reports
|
|
5
|
+
class PercentileReport < BaseReport
|
|
6
|
+
def data
|
|
7
|
+
durations = db.pluck(:duration_ms).compact
|
|
8
|
+
{
|
|
9
|
+
p50: Railswatch::Utils.percentile(durations, 50),
|
|
10
|
+
p95: Railswatch::Utils.percentile(durations, 95),
|
|
11
|
+
p99: Railswatch::Utils.percentile(durations, 99)
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Reports
|
|
5
|
+
class RecentRequestsReport < BaseReport
|
|
6
|
+
def data
|
|
7
|
+
time_ago = Railswatch.recent_requests_time_window.ago
|
|
8
|
+
db.where('occurred_at > ?', time_ago)
|
|
9
|
+
.order(occurred_at: :desc)
|
|
10
|
+
.limit(limit)
|
|
11
|
+
.map(&:record_hash)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def limit
|
|
17
|
+
Railswatch.recent_requests_limit ? Railswatch.recent_requests_limit.to_i : 100_000
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Reports
|
|
5
|
+
class RequestsReport < BaseReport
|
|
6
|
+
def set_defaults
|
|
7
|
+
@set_defaults ||= :count
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def data
|
|
11
|
+
rows = grouped_rows.map { |group_name, grouped| build_row(group_name, grouped) }
|
|
12
|
+
rows.sort_by { |row| -row[sort].to_f }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def grouped_rows
|
|
18
|
+
db.pluck(:controller, :action, :format, :duration_ms, :view_runtime_ms, :db_runtime_ms)
|
|
19
|
+
.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |row, grouped|
|
|
20
|
+
key = group_key(*row.first(3))
|
|
21
|
+
grouped[key] << metrics_hash(row)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def build_row(group_name, rows) # rubocop:disable Metrics/MethodLength
|
|
26
|
+
durations = metric_values(rows, :duration_ms)
|
|
27
|
+
view_runtimes = metric_values(rows, :view_runtime_ms)
|
|
28
|
+
db_runtimes = metric_values(rows, :db_runtime_ms)
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
group: group_name,
|
|
32
|
+
count: rows.size,
|
|
33
|
+
duration_average: average(durations),
|
|
34
|
+
view_runtime_average: average(view_runtimes),
|
|
35
|
+
db_runtime_average: average(db_runtimes),
|
|
36
|
+
duration_slowest: durations.max,
|
|
37
|
+
view_runtime_slowest: view_runtimes.max,
|
|
38
|
+
db_runtime_slowest: db_runtimes.max,
|
|
39
|
+
p50_duration: Railswatch::Utils.percentile(durations, 50),
|
|
40
|
+
p95_duration: Railswatch::Utils.percentile(durations, 95),
|
|
41
|
+
p99_duration: Railswatch::Utils.percentile(durations, 99)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def group_key(controller, action, format)
|
|
46
|
+
case group
|
|
47
|
+
when :controller_action
|
|
48
|
+
"#{controller}##{action}"
|
|
49
|
+
when :controller
|
|
50
|
+
controller
|
|
51
|
+
else
|
|
52
|
+
"#{controller}##{action}|#{format}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def metrics_hash(row)
|
|
57
|
+
{
|
|
58
|
+
duration_ms: row[3],
|
|
59
|
+
view_runtime_ms: row[4],
|
|
60
|
+
db_runtime_ms: row[5]
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def metric_values(rows, key)
|
|
65
|
+
rows.filter_map { |row| row[key] }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def average(values)
|
|
69
|
+
return nil if values.empty?
|
|
70
|
+
|
|
71
|
+
values.sum.to_f / values.size
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|