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.
Files changed (138) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +485 -0
  4. data/Rakefile +37 -0
  5. data/app/assets/config/railswatch_manifest.js +0 -0
  6. data/app/assets/images/activity.svg +13 -0
  7. data/app/assets/images/bot.svg +1 -0
  8. data/app/assets/images/close.svg +13 -0
  9. data/app/assets/images/details.svg +3 -0
  10. data/app/assets/images/download.svg +3 -0
  11. data/app/assets/images/export.svg +13 -0
  12. data/app/assets/images/external.svg +1 -0
  13. data/app/assets/images/git.svg +1 -0
  14. data/app/assets/images/github.svg +1 -0
  15. data/app/assets/images/home.svg +16 -0
  16. data/app/assets/images/import.svg +13 -0
  17. data/app/assets/images/menu.svg +16 -0
  18. data/app/assets/images/moon.svg +3 -0
  19. data/app/assets/images/stat.svg +1 -0
  20. data/app/assets/images/sun.svg +4 -0
  21. data/app/assets/images/user.svg +1 -0
  22. data/app/controllers/railswatch/base_controller.rb +35 -0
  23. data/app/controllers/railswatch/concerns/csv_exportable.rb +31 -0
  24. data/app/controllers/railswatch/railswatch_controller.rb +183 -0
  25. data/app/engine_assets/javascripts/apex_ext.js +30 -0
  26. data/app/engine_assets/javascripts/application.js +9 -0
  27. data/app/engine_assets/javascripts/autoupdate.js +79 -0
  28. data/app/engine_assets/javascripts/charts.js +279 -0
  29. data/app/engine_assets/javascripts/navbar.js +11 -0
  30. data/app/engine_assets/javascripts/panel.js +43 -0
  31. data/app/engine_assets/javascripts/table.js +12 -0
  32. data/app/engine_assets/javascripts/theme.js +43 -0
  33. data/app/engine_assets/stylesheets/panel.css +111 -0
  34. data/app/engine_assets/stylesheets/responsive.css +102 -0
  35. data/app/engine_assets/stylesheets/style.css +960 -0
  36. data/app/helpers/railswatch/railswatch_helper.rb +338 -0
  37. data/app/views/railswatch/_panel.html.erb +15 -0
  38. data/app/views/railswatch/layouts/railswatch.html.erb +81 -0
  39. data/app/views/railswatch/railswatch/_card.html.erb +7 -0
  40. data/app/views/railswatch/railswatch/_chart.html.erb +13 -0
  41. data/app/views/railswatch/railswatch/_crashes_table_content.html.erb +62 -0
  42. data/app/views/railswatch/railswatch/_custom_events_table_content.html.erb +27 -0
  43. data/app/views/railswatch/railswatch/_delayed_job_table_content.html.erb +52 -0
  44. data/app/views/railswatch/railswatch/_export.html.erb +4 -0
  45. data/app/views/railswatch/railswatch/_grape_requests_table_content.html.erb +31 -0
  46. data/app/views/railswatch/railswatch/_overview.html.erb +124 -0
  47. data/app/views/railswatch/railswatch/_rake_tasks_table_content.html.erb +25 -0
  48. data/app/views/railswatch/railswatch/_recent_requests_table_content.html.erb +28 -0
  49. data/app/views/railswatch/railswatch/_recent_row.html.erb +41 -0
  50. data/app/views/railswatch/railswatch/_requests_table_content.html.erb +51 -0
  51. data/app/views/railswatch/railswatch/_sidekiq_jobs_table_content.html.erb +50 -0
  52. data/app/views/railswatch/railswatch/_summary.html.erb +50 -0
  53. data/app/views/railswatch/railswatch/_table.html.erb +30 -0
  54. data/app/views/railswatch/railswatch/_trace.html.erb +78 -0
  55. data/app/views/railswatch/railswatch/crashes.html.erb +2 -0
  56. data/app/views/railswatch/railswatch/custom.html.erb +6 -0
  57. data/app/views/railswatch/railswatch/delayed_job.html.erb +6 -0
  58. data/app/views/railswatch/railswatch/grape.html.erb +6 -0
  59. data/app/views/railswatch/railswatch/index.html.erb +9 -0
  60. data/app/views/railswatch/railswatch/rake.html.erb +6 -0
  61. data/app/views/railswatch/railswatch/recent.html.erb +2 -0
  62. data/app/views/railswatch/railswatch/requests.html.erb +2 -0
  63. data/app/views/railswatch/railswatch/resources.html.erb +28 -0
  64. data/app/views/railswatch/railswatch/sidekiq.html.erb +6 -0
  65. data/app/views/railswatch/railswatch/slow.html.erb +2 -0
  66. data/app/views/railswatch/railswatch/summary.js.erb +3 -0
  67. data/app/views/railswatch/railswatch/trace.js.erb +9 -0
  68. data/app/views/railswatch/shared/_header.html.erb +39 -0
  69. data/app/views/railswatch/shared/_page_header.html.erb +23 -0
  70. data/config/routes.rb +27 -0
  71. data/lib/generators/railswatch/install/USAGE +19 -0
  72. data/lib/generators/railswatch/install/install_generator.rb +46 -0
  73. data/lib/generators/railswatch/install/templates/create_railswatch_tables.rb +140 -0
  74. data/lib/generators/railswatch/install/templates/initializer.rb +87 -0
  75. data/lib/railswatch/data_source.rb +106 -0
  76. data/lib/railswatch/engine.rb +103 -0
  77. data/lib/railswatch/events/record.rb +63 -0
  78. data/lib/railswatch/extensions/trace.rb +14 -0
  79. data/lib/railswatch/extensions/trace_db.rb +21 -0
  80. data/lib/railswatch/gems/custom_ext.rb +38 -0
  81. data/lib/railswatch/gems/delayed_job_ext.rb +70 -0
  82. data/lib/railswatch/gems/grape_ext.rb +64 -0
  83. data/lib/railswatch/gems/rake_ext.rb +69 -0
  84. data/lib/railswatch/gems/sidekiq_ext.rb +55 -0
  85. data/lib/railswatch/instrument/metrics_collector.rb +70 -0
  86. data/lib/railswatch/interface.rb +9 -0
  87. data/lib/railswatch/models/application_record.rb +31 -0
  88. data/lib/railswatch/models/base_record.rb +59 -0
  89. data/lib/railswatch/models/collection.rb +37 -0
  90. data/lib/railswatch/models/custom_record.rb +32 -0
  91. data/lib/railswatch/models/delayed_job_record.rb +39 -0
  92. data/lib/railswatch/models/event_record.rb +11 -0
  93. data/lib/railswatch/models/grape_record.rb +61 -0
  94. data/lib/railswatch/models/rake_record.rb +33 -0
  95. data/lib/railswatch/models/request_record.rb +105 -0
  96. data/lib/railswatch/models/resource_record.rb +33 -0
  97. data/lib/railswatch/models/sidekiq_record.rb +41 -0
  98. data/lib/railswatch/models/trace_record.rb +21 -0
  99. data/lib/railswatch/pruner.rb +47 -0
  100. data/lib/railswatch/rails/middleware.rb +117 -0
  101. data/lib/railswatch/rails/query_builder.rb +20 -0
  102. data/lib/railswatch/reports/annotations_report.rb +13 -0
  103. data/lib/railswatch/reports/base_report.rb +48 -0
  104. data/lib/railswatch/reports/breakdown_report.rb +11 -0
  105. data/lib/railswatch/reports/crash_report.rb +11 -0
  106. data/lib/railswatch/reports/overview_report.rb +88 -0
  107. data/lib/railswatch/reports/percentile_report.rb +16 -0
  108. data/lib/railswatch/reports/recent_requests_report.rb +21 -0
  109. data/lib/railswatch/reports/requests_report.rb +75 -0
  110. data/lib/railswatch/reports/resources_report.rb +42 -0
  111. data/lib/railswatch/reports/response_time_report.rb +27 -0
  112. data/lib/railswatch/reports/slow_requests_report.rb +21 -0
  113. data/lib/railswatch/reports/throughput_report.rb +16 -0
  114. data/lib/railswatch/reports/trace_report.rb +17 -0
  115. data/lib/railswatch/system_monitor/resources_monitor.rb +88 -0
  116. data/lib/railswatch/thread/current_request.rb +37 -0
  117. data/lib/railswatch/utils.rb +58 -0
  118. data/lib/railswatch/version.rb +7 -0
  119. data/lib/railswatch/widgets/base.rb +17 -0
  120. data/lib/railswatch/widgets/card.rb +19 -0
  121. data/lib/railswatch/widgets/chart.rb +33 -0
  122. data/lib/railswatch/widgets/crashes_table.rb +27 -0
  123. data/lib/railswatch/widgets/custom_events_table.rb +48 -0
  124. data/lib/railswatch/widgets/delayed_job_table.rb +31 -0
  125. data/lib/railswatch/widgets/grape_requests_table.rb +31 -0
  126. data/lib/railswatch/widgets/percentile_card.rb +23 -0
  127. data/lib/railswatch/widgets/rake_tasks_table.rb +31 -0
  128. data/lib/railswatch/widgets/recent_requests_table.rb +35 -0
  129. data/lib/railswatch/widgets/requests_table.rb +27 -0
  130. data/lib/railswatch/widgets/resource_chart.rb +116 -0
  131. data/lib/railswatch/widgets/response_time_chart.rb +29 -0
  132. data/lib/railswatch/widgets/sidekiq_jobs_table.rb +31 -0
  133. data/lib/railswatch/widgets/slow_requests_table.rb +33 -0
  134. data/lib/railswatch/widgets/table.rb +43 -0
  135. data/lib/railswatch/widgets/throughput_chart.rb +29 -0
  136. data/lib/railswatch.rb +184 -0
  137. data/lib/tasks/railswatch.rake +9 -0
  138. metadata +445 -0
@@ -0,0 +1,23 @@
1
+ <header class="rm-page-header">
2
+ <div class="rm-page-header__copy">
3
+ <p class="rm-page-header__eyebrow"><%= eyebrow %></p>
4
+ <div class="rm-page-header__title-row">
5
+ <h1 class="rm-page-header__title"><%= title %></h1>
6
+ <% if local_assigns[:badge].present? %>
7
+ <span class="rm-badge"><%= badge %></span>
8
+ <% end %>
9
+ </div>
10
+ <p class="rm-page-header__description"><%= description %></p>
11
+ </div>
12
+
13
+ <div class="rm-page-header__actions">
14
+ <% if local_assigns[:autoupdate_interval].present? %>
15
+ <railswatch-autoupdate interval="<%= autoupdate_interval %>">
16
+ <label class="rm-toggle" id="autoupdate_label">
17
+ <input type="checkbox" />
18
+ <span>Auto-update</span>
19
+ </label>
20
+ </railswatch-autoupdate>
21
+ <% end %>
22
+ </div>
23
+ </header>
data/config/routes.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ Railswatch::Engine.routes.draw do
4
+ get '/' => 'railswatch#index', :as => :railswatch
5
+
6
+ get '/requests' => 'railswatch#requests', :as => :railswatch_requests
7
+ get '/crashes' => 'railswatch#crashes', :as => :railswatch_crashes
8
+ get '/recent' => 'railswatch#recent', :as => :railswatch_recent
9
+ get '/slow' => 'railswatch#slow', :as => :railswatch_slow
10
+
11
+ get '/trace/:id' => 'railswatch#trace', :as => :railswatch_trace
12
+ get '/summary' => 'railswatch#summary', :as => :railswatch_summary
13
+
14
+ get '/sidekiq' => 'railswatch#sidekiq', :as => :railswatch_sidekiq
15
+ get '/delayed_job' => 'railswatch#delayed_job', :as => :railswatch_delayed_job
16
+ get '/grape' => 'railswatch#grape', :as => :railswatch_grape
17
+ get '/rake' => 'railswatch#rake', :as => :railswatch_rake
18
+ get '/custom' => 'railswatch#custom', :as => :railswatch_custom
19
+ get '/resources' => 'railswatch#resources', :as => :railswatch_resources
20
+ end
21
+
22
+ Rails.application.routes.draw do
23
+ mount Railswatch::Engine => Railswatch.mount_at, :as => 'railswatch'
24
+ rescue ArgumentError
25
+ # already added
26
+ # this code exist here because engine not includes routing automatically
27
+ end
@@ -0,0 +1,19 @@
1
+ Description:
2
+ Generates initial config and database migration for railswatch
3
+
4
+ Example:
5
+ bin/rails generate railswatch:install
6
+
7
+ This will create:
8
+ config/initializers/railswatch.rb
9
+ db/migrate/TIMESTAMP_create_railswatch_tables.rb
10
+
11
+ Primary database setup:
12
+ keep `config.database_connection_name = nil`
13
+ bin/rails db:migrate
14
+
15
+ Separate database setup:
16
+ add `railswatch` to config/database.yml
17
+ set `config.database_connection_name = :railswatch`
18
+ bin/rails db:migrate
19
+ bin/rails db:migrate:railswatch
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/migration'
4
+
5
+ module Railswatch
6
+ class InstallGenerator < ::Rails::Generators::Base
7
+ include ::Rails::Generators::Migration
8
+
9
+ source_root File.expand_path('templates', __dir__)
10
+ desc 'Generates initial config for railswatch gem'
11
+
12
+ def copy_initializer_file
13
+ copy_file 'initializer.rb', 'config/initializers/railswatch.rb'
14
+ end
15
+
16
+ def install_migration
17
+ migration_template(
18
+ 'create_railswatch_tables.rb',
19
+ 'db/migrate/create_railswatch_tables.rb'
20
+ )
21
+ end
22
+
23
+ def show_database_setup # rubocop:disable Metrics/MethodLength
24
+ say <<~TEXT
25
+
26
+ railswatch was configured with database-backed storage.
27
+
28
+ Primary database:
29
+ Leave `config.database_connection_name = nil`.
30
+ Run `bin/rails db:migrate`.
31
+
32
+ Separate database:
33
+ 1. Add a `railswatch` database entry to `config/database.yml`
34
+ 2. Set `config.database_connection_name = :railswatch`
35
+ 3. Run `bin/rails db:migrate` for your app database
36
+ 4. Run `bin/rails db:migrate:railswatch`
37
+
38
+ You can prune old data later with `bin/rails railswatch:prune`.
39
+ TEXT
40
+ end
41
+
42
+ def self.next_migration_number(_dirname)
43
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRailswatchTables < ActiveRecord::Migration[7.0]
4
+ def change # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
5
+ create_table :railswatch_request_records do |t|
6
+ t.string :controller
7
+ t.string :action
8
+ t.string :format
9
+ t.string :status
10
+ t.string :http_method
11
+ t.string :path
12
+ t.string :request_id, null: false
13
+ t.float :duration_ms
14
+ t.float :view_runtime_ms
15
+ t.float :db_runtime_ms
16
+ t.string :http_referer
17
+ t.text :custom_data
18
+ t.text :exception
19
+ t.text :backtrace
20
+ t.text :request_context
21
+ t.datetime :occurred_at, null: false
22
+ t.timestamps
23
+ end
24
+
25
+ add_index :railswatch_request_records, :occurred_at
26
+ add_index :railswatch_request_records, :request_id, unique: true
27
+ add_index :railswatch_request_records, %i[status occurred_at],
28
+ name: 'idx_rp_requests_on_status_and_occurred_at'
29
+ add_index :railswatch_request_records, %i[controller action format occurred_at],
30
+ name: 'idx_rp_requests_on_grouping_fields'
31
+
32
+ create_table :railswatch_trace_records do |t|
33
+ t.string :request_id, null: false
34
+ t.text :entries
35
+ t.datetime :occurred_at, null: false
36
+ t.timestamps
37
+ end
38
+
39
+ add_index :railswatch_trace_records, :request_id, unique: true
40
+ add_index :railswatch_trace_records, :occurred_at
41
+
42
+ create_table :railswatch_sidekiq_records do |t|
43
+ t.string :queue
44
+ t.string :worker
45
+ t.string :jid, null: false
46
+ t.integer :enqueued_ati
47
+ t.integer :start_timei
48
+ t.string :status
49
+ t.float :duration_ms
50
+ t.text :message
51
+ t.text :job_args
52
+ t.text :error_backtrace
53
+ t.datetime :occurred_at, null: false
54
+ t.timestamps
55
+ end
56
+
57
+ add_index :railswatch_sidekiq_records, :occurred_at
58
+ add_index :railswatch_sidekiq_records, %i[queue worker occurred_at],
59
+ name: 'idx_rp_sidekiq_on_queue_worker_time'
60
+ add_index :railswatch_sidekiq_records, :jid
61
+
62
+ create_table :railswatch_delayed_job_records do |t|
63
+ t.string :jid
64
+ t.string :source_type
65
+ t.string :class_name
66
+ t.string :method_name
67
+ t.string :status
68
+ t.float :duration_ms
69
+ t.text :job_args
70
+ t.text :error_message
71
+ t.text :error_backtrace
72
+ t.datetime :occurred_at, null: false
73
+ t.timestamps
74
+ end
75
+
76
+ add_index :railswatch_delayed_job_records, :occurred_at
77
+ add_index :railswatch_delayed_job_records, %i[status occurred_at], name: 'idx_rp_delayed_jobs_on_status_time'
78
+
79
+ create_table :railswatch_grape_records do |t|
80
+ t.string :format
81
+ t.string :path
82
+ t.string :status
83
+ t.string :http_method
84
+ t.string :request_id
85
+ t.float :endpoint_render_grape_ms
86
+ t.float :endpoint_run_grape_ms
87
+ t.float :format_response_grape_ms
88
+ t.datetime :occurred_at, null: false
89
+ t.timestamps
90
+ end
91
+
92
+ add_index :railswatch_grape_records, :occurred_at
93
+ add_index :railswatch_grape_records, %i[status occurred_at], name: 'idx_rp_grape_on_status_time'
94
+
95
+ create_table :railswatch_rake_records do |t|
96
+ t.text :task
97
+ t.string :status
98
+ t.float :duration_ms
99
+ t.datetime :occurred_at, null: false
100
+ t.timestamps
101
+ end
102
+
103
+ add_index :railswatch_rake_records, :occurred_at
104
+ add_index :railswatch_rake_records, %i[status occurred_at], name: 'idx_rp_rake_on_status_time'
105
+
106
+ create_table :railswatch_custom_records do |t|
107
+ t.string :tag_name
108
+ t.string :namespace_name
109
+ t.string :status
110
+ t.float :duration_ms
111
+ t.datetime :occurred_at, null: false
112
+ t.timestamps
113
+ end
114
+
115
+ add_index :railswatch_custom_records, :occurred_at
116
+ add_index :railswatch_custom_records, %i[status occurred_at], name: 'idx_rp_custom_on_status_time'
117
+
118
+ create_table :railswatch_resource_records do |t|
119
+ t.string :server
120
+ t.string :context
121
+ t.string :role
122
+ t.text :payload
123
+ t.datetime :occurred_at, null: false
124
+ t.timestamps
125
+ end
126
+
127
+ add_index :railswatch_resource_records, :occurred_at
128
+ add_index :railswatch_resource_records, %i[server context role occurred_at],
129
+ name: 'idx_rp_resources_on_server_context_role_time'
130
+
131
+ create_table :railswatch_event_records do |t|
132
+ t.string :name
133
+ t.text :options
134
+ t.datetime :occurred_at, null: false
135
+ t.timestamps
136
+ end
137
+
138
+ add_index :railswatch_event_records, :occurred_at
139
+ end
140
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Railswatch)
4
+ Railswatch.setup do |config|
5
+ # Use the primary application database by default.
6
+ # To use a dedicated database, define an additional database config and set:
7
+ # config.database_connection_name = :railswatch
8
+ config.database_connection_name = nil
9
+
10
+ # All data we collect
11
+ config.duration = 4.hours
12
+
13
+ # Recent Requests configuration
14
+ config.recent_requests_time_window = 60.minutes
15
+ # config.recent_requests_limit = nil # number of recent requests
16
+
17
+ # Slow Requests configuration
18
+ config.slow_requests_time_window = 4.hours
19
+ # config.slow_requests_limit = 500 # number of slow requests
20
+ config.slow_requests_threshold = 500 # ms
21
+
22
+ config.retention = {
23
+ requests: config.duration,
24
+ sidekiq: config.duration,
25
+ delayed_job: config.duration,
26
+ grape: config.duration,
27
+ rake: config.duration,
28
+ custom: config.duration,
29
+ traces: config.recent_requests_time_window,
30
+ resources: 24.hours,
31
+ events: nil
32
+ }
33
+
34
+ config.debug = false
35
+ config.enabled = true
36
+
37
+ # default path where to mount gem
38
+ config.mount_at = '/railswatch'
39
+
40
+ # protect your Performance Dashboard with HTTP BASIC password
41
+ config.http_basic_authentication_enabled = false
42
+ config.http_basic_authentication_user_name = 'railswatch'
43
+ config.http_basic_authentication_password = 'password12'
44
+
45
+ # if you need an additional rules to check user permissions
46
+ config.verify_access_proc = proc { |_controller| true }
47
+ # for example when you have `current_user`
48
+ # config.verify_access_proc = proc { |controller| controller.current_user && controller.current_user.admin? }
49
+
50
+ # Override engine url options, necessary if hosting under a unique domain
51
+ # config.url_options = {host: "sub.example.com"}
52
+
53
+ # You can ignore endpoints with Rails standard notation controller#action
54
+ # config.ignored_endpoints = ['HomeController#contact']
55
+
56
+ # You can ignore request paths by specifying the beginning of the path.
57
+ # For example, all routes starting with '/admin' can be ignored:
58
+ config.ignored_paths = ['/railswatch']
59
+
60
+ # store custom data for the request
61
+ # config.custom_data_proc = proc do |env|
62
+ # request = Rack::Request.new(env)
63
+ # {
64
+ # email: request.env['warden'].user&.email, # if you are using Devise for example
65
+ # user_agent: request.env['HTTP_USER_AGENT']
66
+ # }
67
+ # end
68
+
69
+ # Attach current user info to each request context (stored filtered alongside IP/params).
70
+ # The proc receives the raw Rack env and should return a plain hash.
71
+ # Sensitive values are filtered automatically — keep this lightweight (IDs, roles, emails).
72
+ # config.current_user_proc = proc do |env|
73
+ # user = env['warden']&.user # Devise / Warden
74
+ # { id: user&.id, email: user&.email } if user
75
+ # end
76
+
77
+ # config home button link
78
+ config.home_link = '/'
79
+ config.skipable_rake_tasks = ['webpacker:compile']
80
+ config.include_rake_tasks = false
81
+ config.include_custom_events = true
82
+
83
+ # If enabled, the system monitor will be displayed on the dashboard
84
+ # to enabled add required gems (see README)
85
+ # config.system_monitor_duration = 24.hours
86
+ end
87
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Railswatch
6
+ class DataSource
7
+ class RelationWrapper
8
+ extend Forwardable
9
+
10
+ def_delegators :@relation,
11
+ :where, :order, :limit, :pluck, :map, :group, :count,
12
+ :average, :maximum, :all, :to_a, :find_by, :each,
13
+ :klass
14
+
15
+ def initialize(relation)
16
+ @relation = relation
17
+ end
18
+
19
+ def data
20
+ @relation.to_a
21
+ end
22
+ end
23
+
24
+ KLASSES = {
25
+ requests: Railswatch::Models::RequestRecord,
26
+ sidekiq: Railswatch::Models::SidekiqRecord,
27
+ delayed_job: Railswatch::Models::DelayedJobRecord,
28
+ grape: Railswatch::Models::GrapeRecord,
29
+ rake: Railswatch::Models::RakeRecord,
30
+ custom: Railswatch::Models::CustomRecord,
31
+ resources: Railswatch::Models::ResourceRecord
32
+ }.freeze
33
+
34
+ attr_reader :query, :klass, :type, :days
35
+
36
+ def initialize(type:, query: {}, days: Railswatch::Utils.days(Railswatch.duration))
37
+ @type = type
38
+ @klass = KLASSES.fetch(type)
39
+ @query = query
40
+ @days = days
41
+ end
42
+
43
+ def db
44
+ scope = klass.all
45
+ scope = apply_time_window(scope)
46
+ RelationWrapper.new(apply_filters(scope))
47
+ end
48
+
49
+ def default?
50
+ query.empty?
51
+ end
52
+
53
+ private
54
+
55
+ def apply_time_window(scope)
56
+ return scope.where(occurred_at: query[:on].all_day) if query[:on].present?
57
+
58
+ now = Railswatch::Utils.kind_of_now
59
+ scope.where(occurred_at: (now - days.days)..now)
60
+ end
61
+
62
+ def apply_filters(scope) # rubocop:disable Metrics/MethodLength
63
+ case type
64
+ when :requests
65
+ apply_request_filters(scope)
66
+ when :resources
67
+ apply_resource_filters(scope)
68
+ when :sidekiq
69
+ apply_sidekiq_filters(scope)
70
+ when :delayed_job, :grape, :rake, :custom
71
+ apply_status_filter(scope)
72
+ else
73
+ scope
74
+ end
75
+ end
76
+
77
+ def apply_request_filters(scope) # rubocop:disable Metrics/AbcSize
78
+ scope = scope.where(controller: query[:controller]) if query[:controller].present?
79
+ scope = scope.where(action: query[:action]) if query[:action].present?
80
+ scope = scope.where(format: query[:format]) if query[:format].present?
81
+ scope = scope.where(status: query[:status].to_s) if query[:status].present?
82
+ scope = scope.where(http_method: query[:method]) if query[:method].present?
83
+ scope = scope.where(path: query[:path]) if query[:path].present?
84
+ scope
85
+ end
86
+
87
+ def apply_resource_filters(scope) # rubocop:disable Metrics/AbcSize
88
+ scope = scope.where(server: query[:server]) if query[:server].present?
89
+ scope = scope.where(context: query[:context]) if query[:context].present?
90
+ scope = scope.where(role: query[:role]) if query[:role].present?
91
+ scope
92
+ end
93
+
94
+ def apply_sidekiq_filters(scope) # rubocop:disable Metrics/AbcSize
95
+ scope = scope.where(queue: query[:queue]) if query[:queue].present?
96
+ scope = scope.where(worker: query[:worker]) if query[:worker].present?
97
+ scope = scope.where(status: query[:status].to_s) if query[:status].present?
98
+ scope
99
+ end
100
+
101
+ def apply_status_filter(scope)
102
+ scope = scope.where(status: query[:status].to_s) if query[:status].present?
103
+ scope
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_view/log_subscriber'
4
+ require_relative 'rails/middleware'
5
+ require_relative 'models/collection'
6
+ require_relative 'instrument/metrics_collector'
7
+ require_relative 'system_monitor/resources_monitor'
8
+
9
+ module Railswatch
10
+ class Engine < ::Rails::Engine
11
+ isolate_namespace Railswatch
12
+
13
+ isolate_assets assets_subdir: 'engine_assets'
14
+
15
+ initializer 'railswatch.resource_monitor' do
16
+ # check required gems are available
17
+ Railswatch._resource_monitor_enabled =
18
+ !(defined?(Sys::Filesystem) && defined?(Sys::CPU) && defined?(GetProcessMem)).nil?
19
+
20
+ next unless Railswatch.enabled
21
+ next if ::Rails.env.test?
22
+ next if $railswatch_running_mode == :console # rubocop:disable Style/GlobalVars
23
+
24
+ # start monitoring
25
+ Railswatch._resource_monitor = Railswatch::SystemMonitor::ResourcesMonitor.new(
26
+ ENV['RAILSWATCH_SERVER_CONTEXT'].presence || 'rails',
27
+ ENV['RAILSWATCH_SERVER_ROLE'].presence || 'web'
28
+ )
29
+ end
30
+
31
+ initializer 'railswatch.middleware' do |app|
32
+ next unless Railswatch.enabled
33
+
34
+ app.middleware.insert_after ActionDispatch::Executor, Railswatch::Rails::Middleware
35
+ # look like it works in reverse order?
36
+ app.middleware.insert_before(
37
+ Railswatch::Rails::Middleware,
38
+ Railswatch::Rails::MiddlewareTraceStorerAndCleanup
39
+ )
40
+
41
+ if defined?(::Sidekiq)
42
+ require_relative 'gems/sidekiq_ext'
43
+
44
+ Sidekiq.configure_server do |config|
45
+ config.server_middleware do |chain|
46
+ chain.add Railswatch::Gems::SidekiqExt
47
+ end
48
+
49
+ config.on(:startup) do
50
+ if $railswatch_running_mode != :console # rubocop:disable Style/GlobalVars
51
+ # stop web monitoring
52
+ # when we run sidekiq it also starts web monitoring (see above)
53
+ Railswatch._resource_monitor.stop_monitoring
54
+ Railswatch._resource_monitor = nil
55
+ # start background monitoring
56
+ Railswatch._resource_monitor = Railswatch::SystemMonitor::ResourcesMonitor.new(
57
+ ENV['RAILSWATCH_SERVER_CONTEXT'].presence || 'sidekiq',
58
+ ENV['RAILSWATCH_SERVER_ROLE'].presence || 'background'
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ if defined?(::Grape)
66
+ require_relative 'gems/grape_ext'
67
+ Railswatch::Gems::GrapeExt.init
68
+ end
69
+
70
+ if defined?(::Delayed::Job)
71
+ require_relative 'gems/delayed_job_ext'
72
+ Railswatch::Gems::DelayedJobExt.init
73
+ end
74
+ end
75
+
76
+ initializer :configure_metrics, after: :initialize_logger do
77
+ next unless Railswatch.enabled
78
+
79
+ ActiveSupport::Notifications.subscribe(
80
+ 'process_action.action_controller',
81
+ Railswatch::Instrument::MetricsCollector.new
82
+ )
83
+ end
84
+
85
+ config.after_initialize do
86
+ next unless Railswatch.enabled
87
+
88
+ Railswatch::Models::ApplicationRecord.reset_storage_connection!
89
+
90
+ ActionView::LogSubscriber.prepend Railswatch::Extensions::View
91
+ ActiveRecord::LogSubscriber.prepend Railswatch::Extensions::Db if defined?(ActiveRecord)
92
+
93
+ if defined?(::Rake::Task) && Railswatch.include_rake_tasks
94
+ require_relative 'gems/rake_ext'
95
+ Railswatch::Gems::RakeExt.init
96
+ end
97
+ end
98
+
99
+ if defined?(::Rails::Console)
100
+ $railswatch_running_mode = :console # rubocop:disable Style/GlobalVars
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railswatch
4
+ module Events
5
+ class Record
6
+ attr_reader :record
7
+
8
+ DEFAULT_COLOR = '#FF00FF'
9
+ DEFAULT_LABEL_COLOR = '#FF00FF'
10
+ DEFAULT_LABEL_ORIENTATION = 'horizontal'
11
+
12
+ class << self
13
+ def create(name:, datetimei: Time.now.to_i, options: {})
14
+ model = Railswatch::Models::EventRecord.create!(
15
+ name: name,
16
+ options: options,
17
+ occurred_at: Railswatch::Utils.from_datetimei(datetimei.to_i)
18
+ )
19
+ new(model)
20
+ end
21
+
22
+ def all
23
+ Railswatch::Models::EventRecord.order(:occurred_at).map { |record| new(record) }
24
+ end
25
+ end
26
+
27
+ def initialize(record)
28
+ @record = record
29
+ end
30
+
31
+ delegate :name, to: :record
32
+
33
+ def datetimei
34
+ record.occurred_at.to_i
35
+ end
36
+
37
+ def options
38
+ (record.options || {}).deep_stringify_keys
39
+ end
40
+
41
+ def value
42
+ {
43
+ name: name,
44
+ datetime: record.occurred_at,
45
+ datetimei: datetimei,
46
+ options: options
47
+ }
48
+ end
49
+
50
+ def to_annotation
51
+ {
52
+ x: datetimei * 1000,
53
+ borderColor: options['borderColor'] || DEFAULT_COLOR,
54
+ label: {
55
+ borderColor: options.dig('label', 'borderColor') || DEFAULT_LABEL_COLOR,
56
+ orientation: options.dig('label', 'orientation') || DEFAULT_LABEL_ORIENTATION,
57
+ text: options.dig('label', 'text') || name
58
+ }
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railswatch
4
+ module Extensions
5
+ module View
6
+ # in env
7
+ # this works if config.log_level = :info
8
+ def info
9
+ CurrentRequest.current.trace({ group: :view, message: yield })
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railswatch
4
+ module Extensions
5
+ module Db
6
+ # in env
7
+ # this works if config.log_level = :debug
8
+ def sql(event)
9
+ sql_text = event.payload[:sql].to_s
10
+ return super if sql_text.match?(/\brailswatch_/i)
11
+
12
+ CurrentRequest.current.trace({
13
+ group: :db,
14
+ duration: event.duration.round(2),
15
+ sql: sql_text
16
+ })
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railswatch
4
+ module Gems
5
+ module CustomExtension
6
+ module_function
7
+
8
+ def measure(tag_name, namespace_name = nil, &)
9
+ return yield unless Railswatch.enabled && Railswatch.include_custom_events
10
+
11
+ now = Railswatch::Utils.time
12
+ status = 'success'
13
+ execute_with_status(&)
14
+ rescue Exception => e # rubocop:disable Lint/RescueException
15
+ status = 'error'
16
+ raise(e)
17
+ ensure
18
+ save_custom_record(tag_name, namespace_name, status, now)
19
+ CurrentRequest.cleanup
20
+ end
21
+
22
+ def execute_with_status
23
+ yield
24
+ end
25
+
26
+ def save_custom_record(tag_name, namespace_name, status, now)
27
+ Railswatch::Models::CustomRecord.new(
28
+ tag_name: tag_name,
29
+ namespace_name: namespace_name,
30
+ status: status,
31
+ duration: (Railswatch::Utils.time - now) * 1000,
32
+ datetime: now.strftime(Railswatch::FORMAT),
33
+ datetimei: now.to_i
34
+ ).save
35
+ end
36
+ end
37
+ end
38
+ end