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,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Gems
|
|
5
|
+
class DelayedJobExt
|
|
6
|
+
class Plugin < ::Delayed::Plugin
|
|
7
|
+
callbacks do |lifecycle|
|
|
8
|
+
lifecycle.around(:invoke_job) do |job, *args, &block|
|
|
9
|
+
now = Railswatch::Utils.time
|
|
10
|
+
error = nil
|
|
11
|
+
block.call(job, *args)
|
|
12
|
+
status = 'success'
|
|
13
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
14
|
+
status = 'error'
|
|
15
|
+
error = e
|
|
16
|
+
raise e
|
|
17
|
+
ensure
|
|
18
|
+
Railswatch::Gems::DelayedJobExt::Plugin.persist(job, now, status, error)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.persist(job, now, status, error) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
23
|
+
meta_data = meta(job.payload_object)
|
|
24
|
+
error_bt = error ? error.backtrace&.take(20)&.join("\n") : nil
|
|
25
|
+
record = Railswatch::Models::DelayedJobRecord.new(
|
|
26
|
+
jid: job.id,
|
|
27
|
+
duration: (Railswatch::Utils.time - now) * 1000,
|
|
28
|
+
datetime: now.strftime(Railswatch::FORMAT),
|
|
29
|
+
datetimei: now.to_i,
|
|
30
|
+
source_type: meta_data[0],
|
|
31
|
+
class_name: meta_data[1],
|
|
32
|
+
method_name: meta_data[2],
|
|
33
|
+
status: status,
|
|
34
|
+
job_args: filtered_args(job.payload_object),
|
|
35
|
+
error_message: error&.message,
|
|
36
|
+
error_backtrace: error_bt
|
|
37
|
+
)
|
|
38
|
+
record.save
|
|
39
|
+
CurrentRequest.cleanup
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# [source_type, class_name, method_name]
|
|
43
|
+
def self.meta(payload_object) # rubocop:disable Metrics/MethodLength
|
|
44
|
+
if payload_object.is_a?(::Delayed::PerformableMethod)
|
|
45
|
+
if payload_object.object.is_a?(Module)
|
|
46
|
+
[:class_method, payload_object.object.name, payload_object.method_name.to_s]
|
|
47
|
+
else
|
|
48
|
+
[:instance_method, payload_object.object.class.name, payload_object.method_name.to_s]
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
[:instance_method, payload_object.class.name, 'perform']
|
|
52
|
+
end
|
|
53
|
+
rescue StandardError
|
|
54
|
+
%i[unknown unknown unknown]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.filtered_args(payload_object)
|
|
58
|
+
raw = payload_object.args if payload_object.respond_to?(:args)
|
|
59
|
+
Railswatch::Utils.filter_params({ 'args' => Array.wrap(raw) })['args']
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.init
|
|
66
|
+
::Delayed::Worker.plugins += [::Railswatch::Gems::DelayedJobExt::Plugin]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Gems
|
|
5
|
+
class GrapeExt
|
|
6
|
+
class << self
|
|
7
|
+
def init
|
|
8
|
+
ActiveSupport::Notifications.subscribe(/grape/) do |name, start, finish, _id, payload|
|
|
9
|
+
handle_grape_notification(name, start, finish, payload)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def handle_grape_notification(name, start, finish, payload)
|
|
14
|
+
req = setup_grape_request
|
|
15
|
+
now = Railswatch::Utils.time
|
|
16
|
+
set_record_fields(req, now)
|
|
17
|
+
set_timing_field(req.record, name, start, finish)
|
|
18
|
+
set_payload_fields(req.record, payload[:env]) if payload[:env]
|
|
19
|
+
save_and_cleanup_if_needed(req, name, payload)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def setup_grape_request
|
|
23
|
+
req = CurrentRequest.current
|
|
24
|
+
req.ignore.add(:monitoring)
|
|
25
|
+
req.data ||= {}
|
|
26
|
+
req.record ||= Railswatch::Models::GrapeRecord.new(request_id: req.request_id)
|
|
27
|
+
req
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def set_record_fields(req, now)
|
|
31
|
+
req.record.datetimei ||= now.to_i
|
|
32
|
+
req.record.datetime ||= now.strftime(Railswatch::FORMAT)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def save_and_cleanup_if_needed(req, name, payload)
|
|
36
|
+
return unless name == 'format_response.grape' || name_grape_expect_no_content?(req, name, payload)
|
|
37
|
+
|
|
38
|
+
req.record.save
|
|
39
|
+
CurrentRequest.cleanup
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def name_grape_expect_no_content?(req, name, payload)
|
|
43
|
+
expects_no_content = Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(req.record.status.to_i)
|
|
44
|
+
name == 'endpoint_run.grape' && (payload[:endpoint]&.body.nil? || expects_no_content)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def set_timing_field(record, name, start, finish)
|
|
48
|
+
return unless ['endpoint_render.grape', 'endpoint_run.grape', 'format_response.grape'].include?(name)
|
|
49
|
+
|
|
50
|
+
record.send("#{name.tr('.', '_')}=", (finish - start) * 1000)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def set_payload_fields(record, env)
|
|
54
|
+
endpoint = env['api.endpoint']
|
|
55
|
+
|
|
56
|
+
record.status = endpoint&.status || env['api.response.status']
|
|
57
|
+
record.format = env['api.format']
|
|
58
|
+
record.method = env['REQUEST_METHOD']
|
|
59
|
+
record.path = env['PATH_INFO']
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Gems
|
|
5
|
+
class RakeExt
|
|
6
|
+
class << self
|
|
7
|
+
def init # rubocop:disable Metrics/MethodLength
|
|
8
|
+
::Rake::Task.class_eval do
|
|
9
|
+
next if method_defined?(:invoke_with_railswatch)
|
|
10
|
+
|
|
11
|
+
def invoke_with_railswatch(*args)
|
|
12
|
+
now = Railswatch::Utils.time
|
|
13
|
+
status = 'success'
|
|
14
|
+
invoke_without_new_railswatch(*args)
|
|
15
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
16
|
+
status = 'error'
|
|
17
|
+
raise(e)
|
|
18
|
+
ensure
|
|
19
|
+
Railswatch::Gems::RakeExt.store_invocation(self, args, now, status)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
alias_method :invoke_without_new_railswatch, :invoke
|
|
23
|
+
alias_method :invoke, :invoke_with_railswatch
|
|
24
|
+
|
|
25
|
+
def invoke(*args) # rubocop:disable Lint/DuplicateMethods
|
|
26
|
+
invoke_with_railswatch(*args)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_task_name(*args)
|
|
32
|
+
(ARGV + args).compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def store_invocation(task, args, started_at, status)
|
|
36
|
+
return if Railswatch.skipable_rake_tasks.include?(task.name)
|
|
37
|
+
return unless monitoring_storage_available?
|
|
38
|
+
|
|
39
|
+
build_rake_record(task, args, started_at, status).save
|
|
40
|
+
ensure
|
|
41
|
+
CurrentRequest.cleanup
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolved_task_name(task, args)
|
|
45
|
+
task_info = find_task_name(*args)
|
|
46
|
+
task_info.empty? ? [task.name] : task_info
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def monitoring_storage_available?
|
|
50
|
+
Railswatch::Models::RakeRecord.connection.data_source_exists?(
|
|
51
|
+
Railswatch::Models::RakeRecord.table_name
|
|
52
|
+
)
|
|
53
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build_rake_record(task, args, started_at, status)
|
|
58
|
+
Railswatch::Models::RakeRecord.new(
|
|
59
|
+
task: resolved_task_name(task, args),
|
|
60
|
+
datetime: started_at.strftime(Railswatch::FORMAT),
|
|
61
|
+
datetimei: started_at.to_i,
|
|
62
|
+
duration: (Railswatch::Utils.time - started_at) * 1000,
|
|
63
|
+
status: status
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Gems
|
|
5
|
+
class SidekiqExt
|
|
6
|
+
def initialize(options = nil); end
|
|
7
|
+
|
|
8
|
+
def call(worker, msg, queue) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
9
|
+
now = Railswatch::Utils.time
|
|
10
|
+
payload = msg.is_a?(Hash) ? msg : {}
|
|
11
|
+
queue_name = queue.respond_to?(:call) ? nil : queue
|
|
12
|
+
record = build_record(worker, payload, queue_name, now)
|
|
13
|
+
|
|
14
|
+
result = yield
|
|
15
|
+
record.status = 'success'
|
|
16
|
+
result
|
|
17
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
18
|
+
record.status = 'exception'
|
|
19
|
+
record.message = e.message
|
|
20
|
+
record.error_backtrace = e.backtrace&.take(20)&.join("\n")
|
|
21
|
+
raise e
|
|
22
|
+
ensure
|
|
23
|
+
persist_record(record, now)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def persist_record(record, started_at)
|
|
29
|
+
return unless record
|
|
30
|
+
|
|
31
|
+
record.duration = (Railswatch::Utils.time - started_at) * 1000
|
|
32
|
+
record.save
|
|
33
|
+
CurrentRequest.cleanup
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_record(worker, payload, queue_name, now) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
37
|
+
filtered = Railswatch::Utils.filter_params({ 'args' => Array.wrap(payload['args']) })
|
|
38
|
+
Railswatch::Models::SidekiqRecord.new(
|
|
39
|
+
enqueued_ati: present_to_i(payload['enqueued_at']),
|
|
40
|
+
datetimei: present_to_i(payload['created_at']),
|
|
41
|
+
jid: payload['jid'].presence || SecureRandom.hex(12),
|
|
42
|
+
queue: queue_name.presence || 'default',
|
|
43
|
+
start_timei: now.to_i,
|
|
44
|
+
datetime: now.strftime(Railswatch::FORMAT),
|
|
45
|
+
worker: payload['wrapped'].presence || worker.to_s,
|
|
46
|
+
job_args: filtered['args']
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def present_to_i(val)
|
|
51
|
+
val.present? ? val.to_i : nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Instrument
|
|
5
|
+
class MetricsCollector
|
|
6
|
+
# payload
|
|
7
|
+
# {
|
|
8
|
+
# controller: "PostsController",
|
|
9
|
+
# action: "index",
|
|
10
|
+
# params: {"action" => "index", "controller" => "posts"},
|
|
11
|
+
# headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
|
|
12
|
+
# format: :html,
|
|
13
|
+
# method: "GET",
|
|
14
|
+
# path: "/posts",
|
|
15
|
+
# status: 200,
|
|
16
|
+
# view_runtime: 46.848,
|
|
17
|
+
# db_runtime: 0.157
|
|
18
|
+
# }
|
|
19
|
+
|
|
20
|
+
def call(event_name, started, finished, event_id, payload)
|
|
21
|
+
return if Railswatch.skip
|
|
22
|
+
return if CurrentRequest.current.data
|
|
23
|
+
|
|
24
|
+
return if ignored_event?(payload)
|
|
25
|
+
|
|
26
|
+
CurrentRequest.current.data = build_record(
|
|
27
|
+
event_name: event_name,
|
|
28
|
+
started: started,
|
|
29
|
+
finished: finished,
|
|
30
|
+
event_id: event_id,
|
|
31
|
+
payload: payload
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def ignored_event?(payload)
|
|
38
|
+
return true if payload[:controller].blank?
|
|
39
|
+
|
|
40
|
+
endpoint = "#{payload[:controller]}##{payload[:action]}"
|
|
41
|
+
Railswatch.ignored_endpoints.include?(endpoint) ||
|
|
42
|
+
Railswatch.ignored_paths.any? { |path| payload[:path].start_with?(path) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_record(event_name:, started:, finished:, event_id:, payload:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
46
|
+
event = notification_event(event_name, started, finished, event_id, payload)
|
|
47
|
+
finished_at = finished.is_a?(Time) ? finished.utc : Time.at(finished).utc
|
|
48
|
+
{
|
|
49
|
+
controller: payload[:controller],
|
|
50
|
+
action: payload[:action],
|
|
51
|
+
format: payload[:format],
|
|
52
|
+
status: payload[:status],
|
|
53
|
+
datetime: finished_at.strftime(Railswatch::FORMAT),
|
|
54
|
+
datetimei: finished_at.to_i,
|
|
55
|
+
method: payload[:method],
|
|
56
|
+
path: payload[:path],
|
|
57
|
+
view_runtime: payload[:view_runtime],
|
|
58
|
+
db_runtime: payload[:db_runtime],
|
|
59
|
+
duration: event.duration,
|
|
60
|
+
exception: payload[:exception],
|
|
61
|
+
exception_object: payload[:exception_object]
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def notification_event(event_name, started, finished, event_id, payload)
|
|
66
|
+
ActiveSupport::Notifications::Event.new(event_name, started, finished, event_id, payload)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
6
|
+
self.abstract_class = true
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def reset_storage_connection!
|
|
10
|
+
target = Railswatch.database_connection_name.presence&.to_s
|
|
11
|
+
if target.blank?
|
|
12
|
+
remove_connection
|
|
13
|
+
else
|
|
14
|
+
establish_connection(resolve_connection_name(target).to_sym)
|
|
15
|
+
end
|
|
16
|
+
rescue ActiveRecord::ConnectionNotEstablished
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def resolve_connection_name(target)
|
|
23
|
+
configs = ActiveRecord::Base.configurations.configs_for(env_name: ::Rails.env)
|
|
24
|
+
return target if configs.any? { |config| config.name == target }
|
|
25
|
+
|
|
26
|
+
"#{::Rails.env}_#{target}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Railswatch
|
|
6
|
+
module Models
|
|
7
|
+
class BaseRecord < ApplicationRecord
|
|
8
|
+
self.abstract_class = true
|
|
9
|
+
|
|
10
|
+
before_validation :normalize_occurred_at!
|
|
11
|
+
|
|
12
|
+
def datetime
|
|
13
|
+
occurred_at&.utc&.strftime(Railswatch::FORMAT)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def datetime=(value)
|
|
17
|
+
@legacy_datetime = value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def datetimei
|
|
21
|
+
occurred_at&.to_i
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def datetimei=(value)
|
|
25
|
+
@legacy_datetimei = value&.to_i
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def value
|
|
29
|
+
payload_hash
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def duration
|
|
33
|
+
value['duration']
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def payload_hash
|
|
39
|
+
{}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ms(value)
|
|
43
|
+
"#{value.to_f.round(1)} ms" if value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def normalize_occurred_at!
|
|
47
|
+
self.occurred_at ||= if @legacy_datetimei.present?
|
|
48
|
+
Time.at(@legacy_datetimei, in: '+00:00')
|
|
49
|
+
elsif @legacy_datetime.present?
|
|
50
|
+
Time.strptime(@legacy_datetime, Railswatch::FORMAT).utc
|
|
51
|
+
else
|
|
52
|
+
Railswatch::Utils.time
|
|
53
|
+
end
|
|
54
|
+
rescue ArgumentError
|
|
55
|
+
self.occurred_at ||= Railswatch::Utils.time
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class Collection
|
|
6
|
+
attr_reader :data
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@data = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add(record)
|
|
13
|
+
@data << record
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def group_by(type)
|
|
17
|
+
case type
|
|
18
|
+
when :controller_action, :controller_action_format, :datetime, :path
|
|
19
|
+
fetch_values @data.group_by(&type)
|
|
20
|
+
else
|
|
21
|
+
{}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def fetch_values(groupped_collection)
|
|
26
|
+
result = {}
|
|
27
|
+
groupped_collection.each do |key, records|
|
|
28
|
+
result[key] ||= []
|
|
29
|
+
records.each do |record|
|
|
30
|
+
result[key] << record.value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class CustomRecord < BaseRecord
|
|
6
|
+
self.table_name = 'railswatch_custom_records'
|
|
7
|
+
|
|
8
|
+
def duration
|
|
9
|
+
duration_ms
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def duration=(value)
|
|
13
|
+
self.duration_ms = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def payload_hash
|
|
17
|
+
{ 'duration' => duration_ms }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def record_hash
|
|
21
|
+
{
|
|
22
|
+
tag_name: tag_name,
|
|
23
|
+
namespace_name: namespace_name,
|
|
24
|
+
status: status,
|
|
25
|
+
datetimei: occurred_at.to_i,
|
|
26
|
+
datetime: occurred_at,
|
|
27
|
+
duration: duration_ms
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class DelayedJobRecord < BaseRecord
|
|
6
|
+
self.table_name = 'railswatch_delayed_job_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
|
+
{ 'duration' => duration_ms }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record_hash # rubocop:disable Metrics/MethodLength
|
|
23
|
+
{
|
|
24
|
+
jid: jid,
|
|
25
|
+
datetime: occurred_at,
|
|
26
|
+
datetimei: occurred_at.to_i,
|
|
27
|
+
duration: duration_ms,
|
|
28
|
+
status: status,
|
|
29
|
+
source_type: source_type,
|
|
30
|
+
class_name: class_name,
|
|
31
|
+
method_name: method_name,
|
|
32
|
+
job_args: job_args,
|
|
33
|
+
error_message: error_message,
|
|
34
|
+
error_backtrace: error_backtrace
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class GrapeRecord < BaseRecord
|
|
6
|
+
self.table_name = 'railswatch_grape_records'
|
|
7
|
+
|
|
8
|
+
def method
|
|
9
|
+
http_method
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def method=(value)
|
|
13
|
+
self.http_method = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def endpoint_render_grape
|
|
17
|
+
endpoint_render_grape_ms
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def endpoint_render_grape=(value)
|
|
21
|
+
self.endpoint_render_grape_ms = value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def endpoint_run_grape
|
|
25
|
+
endpoint_run_grape_ms
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def endpoint_run_grape=(value)
|
|
29
|
+
self.endpoint_run_grape_ms = value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_response_grape
|
|
33
|
+
format_response_grape_ms
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def format_response_grape=(value)
|
|
37
|
+
self.format_response_grape_ms = value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def payload_hash
|
|
41
|
+
{
|
|
42
|
+
'endpoint_render.grape' => endpoint_render_grape_ms,
|
|
43
|
+
'endpoint_run.grape' => endpoint_run_grape_ms,
|
|
44
|
+
'format_response.grape' => format_response_grape_ms
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def record_hash
|
|
49
|
+
{
|
|
50
|
+
format: self.format,
|
|
51
|
+
status: status,
|
|
52
|
+
method: http_method,
|
|
53
|
+
path: path,
|
|
54
|
+
datetime: occurred_at,
|
|
55
|
+
datetimei: occurred_at.to_i,
|
|
56
|
+
request_id: request_id
|
|
57
|
+
}.merge(payload_hash)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railswatch
|
|
4
|
+
module Models
|
|
5
|
+
class RakeRecord < BaseRecord
|
|
6
|
+
self.table_name = 'railswatch_rake_records'
|
|
7
|
+
|
|
8
|
+
serialize :task, 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
|
+
{ 'duration' => duration_ms }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record_hash
|
|
23
|
+
{
|
|
24
|
+
task: Array.wrap(task),
|
|
25
|
+
datetime: occurred_at,
|
|
26
|
+
datetimei: occurred_at.to_i,
|
|
27
|
+
duration: duration_ms,
|
|
28
|
+
status: status
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|