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,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railswatch
4
+ module RailswatchHelper
5
+ NAVIGATION_ITEMS = [
6
+ { section: :dashboard, label: 'Overview', route: :railswatch_url },
7
+ { section: :requests, label: 'Requests', route: :railswatch_requests_url },
8
+ { section: :recent, label: 'Recent', route: :railswatch_recent_url },
9
+ { section: :slow, label: 'Slow', route: :railswatch_slow_url },
10
+ { section: :crashes, label: 'Errors', route: :railswatch_crashes_url },
11
+ {
12
+ section: :resources,
13
+ label: 'System',
14
+ route: :railswatch_resources_url,
15
+ visible: -> { Railswatch._resource_monitor_enabled }
16
+ },
17
+ {
18
+ section: :sidekiq,
19
+ label: 'Sidekiq',
20
+ route: :railswatch_sidekiq_url,
21
+ visible: -> { defined?(Sidekiq) }
22
+ },
23
+ {
24
+ section: :delayed_job,
25
+ label: 'Delayed Job',
26
+ route: :railswatch_delayed_job_url,
27
+ visible: -> { defined?(Delayed::Job) }
28
+ },
29
+ {
30
+ section: :grape,
31
+ label: 'Grape',
32
+ route: :railswatch_grape_url,
33
+ visible: -> { defined?(Grape) }
34
+ },
35
+ {
36
+ section: :rake,
37
+ label: 'Rake',
38
+ route: :railswatch_rake_url,
39
+ visible: -> { Railswatch.include_rake_tasks }
40
+ },
41
+ {
42
+ section: :custom,
43
+ label: 'Custom',
44
+ route: :railswatch_custom_url,
45
+ visible: -> { Railswatch.include_custom_events }
46
+ }
47
+ ].freeze
48
+
49
+ PAGE_META = {
50
+ index: {
51
+ eyebrow: 'Performance',
52
+ title: 'Application Command Center',
53
+ description: lambda {
54
+ "Live request health, latency, and throughput over the last #{human_window(Railswatch.duration)}."
55
+ },
56
+ badge: 'Live'
57
+ },
58
+ requests: {
59
+ eyebrow: 'Analysis',
60
+ title: 'Request Breakdown',
61
+ description: 'Compare controllers and actions by volume, percentiles, averages, and slowest paths.'
62
+ },
63
+ recent: {
64
+ eyebrow: 'Realtime',
65
+ title: 'Recent Requests',
66
+ description: lambda {
67
+ "Inspect fresh traffic sampled from the last #{human_window(Railswatch.recent_requests_time_window)}."
68
+ }
69
+ },
70
+ slow: {
71
+ eyebrow: 'Latency',
72
+ title: 'Slow Requests',
73
+ description: lambda {
74
+ "Focus on requests above #{Railswatch.slow_requests_threshold} ms " \
75
+ 'and inspect traces before they age out.'
76
+ }
77
+ },
78
+ crashes: {
79
+ eyebrow: 'Reliability',
80
+ title: '500 Error Timeline',
81
+ description: 'Review failures, backtraces, and the request context that led to them.'
82
+ },
83
+ resources: {
84
+ eyebrow: 'Infrastructure',
85
+ title: 'System Resources',
86
+ description: lambda {
87
+ 'Watch CPU, memory, and disk behavior across the last ' \
88
+ "#{human_window(Railswatch.system_monitor_duration)}."
89
+ }
90
+ },
91
+ sidekiq: {
92
+ eyebrow: 'Async Jobs',
93
+ title: 'Sidekiq Workload',
94
+ description: 'Track job throughput, worker runtime, and recent executions.'
95
+ },
96
+ delayed_job: {
97
+ eyebrow: 'Async Jobs',
98
+ title: 'Delayed Job Workload',
99
+ description: 'Monitor queue throughput, execution time, and recent jobs.'
100
+ },
101
+ grape: {
102
+ eyebrow: 'API',
103
+ title: 'Grape Endpoints',
104
+ description: 'Keep an eye on API traffic volume and recent endpoint activity.'
105
+ },
106
+ rake: {
107
+ eyebrow: 'Automation',
108
+ title: 'Rake Tasks',
109
+ description: 'Surface task runtime and throughput for your scheduled or manual jobs.'
110
+ },
111
+ custom: {
112
+ eyebrow: 'Business Events',
113
+ title: 'Custom Measurements',
114
+ description: 'Explore custom event timing and throughput captured with Railswatch.measure.'
115
+ },
116
+ default: {
117
+ eyebrow: 'Monitoring',
118
+ title: 'Railswatch',
119
+ description: 'Observe the health of your Rails application.'
120
+ }
121
+ }.freeze
122
+
123
+ def navigation_items
124
+ NAVIGATION_ITEMS.filter_map do |item|
125
+ next unless navigation_item_visible?(item)
126
+
127
+ item.slice(:section, :label).merge(path: railswatch.public_send(item[:route]))
128
+ end
129
+ end
130
+
131
+ def page_meta(section = action_name.to_sym)
132
+ resolve_page_meta(PAGE_META[section.to_sym] || PAGE_META[:default])
133
+ end
134
+
135
+ def round_it(value, limit = 1)
136
+ return nil unless value
137
+ return value if value.is_a?(Integer)
138
+
139
+ value.nan? ? nil : value.round(limit)
140
+ end
141
+
142
+ def duration_alert_class(duration_str) # rubocop:disable Metrics/MethodLength
143
+ if duration_str.to_s =~ /(\d+.?\d+?)/
144
+ duration = ::Regexp.last_match(1).to_f
145
+ if duration >= 500
146
+ 'has-background-danger has-text-white-bis'
147
+ elsif duration >= 200
148
+ 'has-background-warning has-text-black-ter'
149
+ else
150
+ 'has-background-success has-text-white-bis'
151
+ end
152
+ else
153
+ 'has-background-light'
154
+ end
155
+ end
156
+
157
+ def extract_duration(str)
158
+ return str[:duration].to_s if str.is_a?(Hash) && str[:duration]
159
+
160
+ if str =~ /Duration: (\d+.?\d+?ms)/i
161
+ ::Regexp.last_match(1)
162
+ else
163
+ '-'
164
+ end
165
+ end
166
+
167
+ def ms(value, limit = 1)
168
+ result = round_it(value, limit)
169
+ return '-' if result.nil?
170
+
171
+ result && result != 0 ? "#{result} ms" : '< 0 ms'
172
+ end
173
+
174
+ def compact_number(value)
175
+ return '0' if value.blank?
176
+
177
+ number_to_human(value, precision: 3, strip_insignificant_zeros: true)
178
+ end
179
+
180
+ def percentage(value, precision = 1)
181
+ number_to_percentage(value.to_f, precision: precision)
182
+ end
183
+
184
+ def health_tone(value)
185
+ return 'neutral' if value.nil?
186
+ return 'critical' if value >= 5
187
+ return 'warning' if value >= 1
188
+
189
+ 'healthy'
190
+ end
191
+
192
+ def card_tone(label)
193
+ case label.to_s.downcase
194
+ when 'p50'
195
+ 'healthy'
196
+ when 'p95'
197
+ 'warning'
198
+ when 'p99'
199
+ 'critical'
200
+ else
201
+ 'neutral'
202
+ end
203
+ end
204
+
205
+ def card_caption(label)
206
+ case label.to_s.downcase
207
+ when 'p50'
208
+ 'Median request latency'
209
+ when 'p95'
210
+ 'Tail latency for slower traffic'
211
+ when 'p99'
212
+ 'Worst-case request experience'
213
+ else
214
+ 'Request insight'
215
+ end
216
+ end
217
+
218
+ def short_path(path, length: 55)
219
+ content_tag :span, title: path do
220
+ truncate(path, length: length)
221
+ end
222
+ end
223
+
224
+ def link_to_path(event)
225
+ if event[:method] == 'GET'
226
+ link_to(short_path(event[:path]), event[:path], target: '_blank', title: short_path(event[:path]))
227
+ else
228
+ short_path(event[:path])
229
+ end
230
+ end
231
+
232
+ def report_name(hash)
233
+ hash.except(:on).collect do |key, value|
234
+ next if value.blank?
235
+
236
+ %(
237
+ <div class="control">
238
+ <span class="tags has-addons">
239
+ <span class="tag">#{key}</span>
240
+ <span class="tag is-info is-light">#{value}</span>
241
+ </span>
242
+ </div>)
243
+ end.compact.join.html_safe
244
+ end
245
+
246
+ def status_tag(status) # rubocop:disable Metrics/MethodLength
247
+ klass = case status.to_s
248
+ when /error/, /^5/
249
+ 'tag is-danger'
250
+ when /^4/
251
+ 'tag is-warning'
252
+ when /^3/
253
+ 'tag is-info'
254
+ when /^2/, /success/
255
+ 'tag is-success'
256
+ end
257
+ content_tag(:span, class: klass) do
258
+ status
259
+ end
260
+ end
261
+
262
+ def bot_icon(user_agent) # rubocop:disable Metrics/MethodLength
263
+ return nil if user_agent.blank?
264
+
265
+ browser = Browser.new(user_agent)
266
+ if browser.bot?
267
+ content_tag(:span, class: 'user-agent-icon', title: browser.bot&.name) do
268
+ icon('bot')
269
+ end
270
+ else
271
+ content_tag(:span, class: 'user-agent-icon user-agent-icon-user', title: 'Real User') do
272
+ icon('user')
273
+ end
274
+ end
275
+ end
276
+
277
+ def icon(name)
278
+ @icons ||= {}
279
+
280
+ # https://www.iconfinder.com/iconsets/vivid
281
+ @icons[name] ||= raw File.read(File.expand_path(File.dirname(__FILE__) + "/../../assets/images/#{name}.svg"))
282
+ end
283
+
284
+ def format_rm_datetime(event)
285
+ dt = Railswatch::Reports::BaseReport.time_in_app_time_zone(event)
286
+ I18n.l(dt, format: '%Y-%m-%d %H:%M:%S')
287
+ end
288
+
289
+ # Keep this method permissive because host applications may already call
290
+ # `format_datetime` with arbitrary values in their own templates.
291
+ def format_datetime(event = nil, **)
292
+ return event.to_s unless event.respond_to?(:in_time_zone) || event.respond_to?(:utc)
293
+
294
+ format_rm_datetime(event)
295
+ end
296
+
297
+ def active?(section)
298
+ actions = {
299
+ dashboard: 'index', crashes: 'crashes',
300
+ requests: 'requests', resources: 'resources',
301
+ recent: 'recent', slow: 'slow',
302
+ sidekiq: 'sidekiq', delayed_job: 'delayed_job',
303
+ grape: 'grape', rake: 'rake', custom: 'custom'
304
+ }
305
+ return false unless controller_name == 'railswatch'
306
+
307
+ 'is-active' if action_name == actions[section]
308
+ end
309
+
310
+ private
311
+
312
+ def navigation_item_visible?(item)
313
+ visibility_rule = item[:visible]
314
+ visibility_rule.nil? || instance_exec(&visibility_rule)
315
+ end
316
+
317
+ def resolve_page_meta(meta)
318
+ meta.transform_values do |value|
319
+ value.respond_to?(:call) ? instance_exec(&value) : value
320
+ end
321
+ end
322
+
323
+ def human_window(duration)
324
+ seconds = duration.to_i
325
+ return '0 minutes' if seconds <= 0
326
+
327
+ parts = []
328
+ { day: 1.day, hour: 1.hour, minute: 1.minute }.each do |name, unit|
329
+ next if seconds < unit
330
+
331
+ value, seconds = seconds.divmod(unit)
332
+ parts << "#{value} #{name}#{'s' if value != 1}"
333
+ end
334
+
335
+ parts.first(2).join(' ')
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,15 @@
1
+ <div class="cd-panel cd-panel--from-right js-cd-panel-main" data-skip-autoupdate>
2
+ <div class="cd-panel__container">
3
+ <nav class="panel">
4
+ <p class="panel-heading">
5
+ <span class="field is-grouped is-grouped-multiline"></span>
6
+ </p>
7
+ <div class="panel-block">
8
+ <div class="cd-panel__content">
9
+ </div> <!-- cd-panel__content -->
10
+ </div>
11
+ </nav>
12
+ </div> <!-- cd-panel__container -->
13
+ </div> <!-- cd-panel -->
14
+
15
+ <div class="panel-overlay" data-skip-autoupdate></div>
@@ -0,0 +1,81 @@
1
+ <!DOCTYPE html>
2
+ <html data-theme="light">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Railswatch</title>
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag if ::Rails::VERSION::STRING.to_f >= 5.2 %>
9
+
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
13
+ <script>
14
+ (() => {
15
+ try {
16
+ const savedTheme = localStorage.getItem('railswatch:theme');
17
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
18
+ document.documentElement.dataset.theme = savedTheme || systemTheme;
19
+ } catch (_error) {
20
+ document.documentElement.dataset.theme = 'light';
21
+ }
22
+ })();
23
+ </script>
24
+
25
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
26
+ <%= Railswatch.stylesheet_link_tag "style" %>
27
+ <%= Railswatch.stylesheet_link_tag "panel" %>
28
+ <%= Railswatch.stylesheet_link_tag "responsive" %>
29
+
30
+ <link rel="shortcut icon" href="/favicon.ico">
31
+
32
+ <%= Railswatch.javascript_importmap_tags "application", {
33
+ "@rails/ujs" => "https://cdn.jsdelivr.net/npm/@rails/ujs@7.1.3-4/+esm",
34
+ "jquery" => "https://cdn.jsdelivr.net/npm/jquery@3.7.1/+esm",
35
+ "apexcharts" => "https://cdn.jsdelivr.net/npm/apexcharts@3.45.0/+esm",
36
+ "ms" => "https://cdn.jsdelivr.net/npm/ms@2.1.3/+esm",
37
+ "stupid-table-plugin" => "https://cdn.jsdelivr.net/npm/stupid-table-plugin@1.1.3/+esm",
38
+ "idiomorph" => "https://cdn.jsdelivr.net/npm/idiomorph@0.7.4/+esm"
39
+ } %>
40
+
41
+ <script>
42
+ window.annotationsData = <%= raw Railswatch::Reports::AnnotationsReport.new.data.to_json %>;
43
+ window.railswatchDuration = <%= Railswatch.duration.to_i * 1000 %>;
44
+ </script>
45
+ </head>
46
+
47
+ <body class="has-sticky-footer rm-body">
48
+ <div class="loader-wrapper">
49
+ <div class="loader is-loading"></div>
50
+ </div>
51
+
52
+ <div class="rm-backdrop" aria-hidden="true">
53
+ <div class="rm-backdrop__orb rm-backdrop__orb--one"></div>
54
+ <div class="rm-backdrop__orb rm-backdrop__orb--two"></div>
55
+ <div class="rm-backdrop__grid"></div>
56
+ </div>
57
+
58
+ <div class="main-content rm-main-content">
59
+ <section class="section rm-shell">
60
+ <div class="container is-fluid is-size-7 rm-container">
61
+ <%= render '/railswatch/shared/header' %>
62
+ <main class="rm-content-shell">
63
+ <%= yield %>
64
+ </main>
65
+ </div>
66
+ </section>
67
+ </div>
68
+
69
+ <footer class="footer footer-box rm-footer">
70
+ <div class="container is-fluid is-size-7 rm-footer__inner">
71
+ <div>
72
+ © Railswatch
73
+ </div>
74
+ <%= link_to 'https://github.com/100rabhg/railswatch', 'https://github.com/100rabhg/railswatch', target: '_blank' %>
75
+ <div>v<%= Railswatch::VERSION %></div>
76
+ </div>
77
+ </footer>
78
+
79
+ <%= render '/railswatch/panel' %>
80
+ </body>
81
+ </html>
@@ -0,0 +1,7 @@
1
+ <div class="widget">
2
+ <section class="rm-surface rm-widget-card rm-metric-card" data-tone="<%= card_tone(card.label) %>">
3
+ <div class="rm-metric-card__label"><%= card.label %></div>
4
+ <strong class="rm-metric-card__value"><%= ms card.value %></strong>
5
+ <p class="rm-metric-card__caption"><%= card_caption(card.label) %></p>
6
+ </section>
7
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="widget">
2
+ <section class="rm-surface rm-widget-card">
3
+ <header class="rm-widget-card__header">
4
+ <div>
5
+ <h2 class="rm-widget-card__title"><%= chart.subtitle %></h2>
6
+ </div>
7
+ </header>
8
+ <div class="rm-widget-card__body">
9
+ <%= tag.railswatch_chart(chart.data.to_json.html_safe, id: chart.id, type: chart.type, legend: chart.legend, units: chart.units, class: "chart") %>
10
+ <p class="rm-widget-card__description"><%= chart.description %></p>
11
+ </div>
12
+ </section>
13
+ </div>
@@ -0,0 +1,62 @@
1
+ <table class="<%= table.table_classes %>">
2
+ <thead>
3
+ <tr>
4
+ <th></th>
5
+ <th data-sort="string">Datetime</th>
6
+ <th data-sort="string">Controller#action</th>
7
+ <th data-sort="string">Method</th>
8
+ <th data-sort="string">Format</th>
9
+ <th></th>
10
+ <th data-sort="string">Path</th>
11
+ <th data-sort="string">Exception</th>
12
+ <th data-sort="string">Status</th>
13
+ <th data-sort="float">Duration</th>
14
+ <th data-sort="float">Views</th>
15
+ <th data-sort="float">DB</th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <% if table.data.empty? %>
20
+ <tr>
21
+ <td colspan="12"><%= table.empty_message %></td>
22
+ </tr>
23
+ <% end %>
24
+
25
+ <% table.data.each do |e| %>
26
+ <tr>
27
+ <td>
28
+ <% if e[:request_id].present? %>
29
+ <%= link_to railswatch.railswatch_trace_path(id: e[:request_id]), remote: true do %>
30
+ <span class="stats_icon_max"><%= icon 'activity' %></span>
31
+ <% end %>
32
+ <% end %>
33
+ </td>
34
+ <td><%= format_rm_datetime e[:datetime] %></td>
35
+ <td>
36
+ <%= link_to railswatch.railswatch_summary_path({controller_eq: e[:controller], action_eq: e[:action]}), remote: true do %>
37
+ <span class="details_icon">
38
+ <%= icon 'details' %>
39
+ </span>
40
+ <%= e[:controller] + '#' + e[:action] %>
41
+ <% end %>
42
+ </td>
43
+ <td><%= e[:method] %></td>
44
+ <td><%= e[:format] %></td>
45
+ <td><%= bot_icon e["user_agent"] %></td>
46
+ <td>
47
+ <span class="with_external_icon">
48
+ <%= link_to_path(e) %>
49
+ <span class="icon external_icon">
50
+ <%= icon('external') %>
51
+ </span>
52
+ </span>
53
+ </td>
54
+ <td><%= e[:exception] %></td>
55
+ <td><%= status_tag e[:status] %></td>
56
+ <td class="nowrap"><%= ms e[:duration] %></td>
57
+ <td class="nowrap"><%= ms e[:view_runtime] %></td>
58
+ <td class="nowrap"><%= ms e[:db_runtime] %></td>
59
+ </tr>
60
+ <% end %>
61
+ </tbody>
62
+ </table>
@@ -0,0 +1,27 @@
1
+ <table class="<%= table.table_classes %>">
2
+ <thead>
3
+ <tr>
4
+ <th data-sort="string">Datetime</th>
5
+ <th data-sort="string">Tag</th>
6
+ <th data-sort="string">Namespace</th>
7
+ <th data-sort="string">Status</th>
8
+ <th data-sort="float">Duration</th>
9
+ </tr>
10
+ </thead>
11
+ <tbody>
12
+ <% if table.data.empty? %>
13
+ <tr>
14
+ <td colspan="5"><%= table.empty_message %></td>
15
+ </tr>
16
+ <% end %>
17
+ <% table.data.each do |e| %>
18
+ <tr>
19
+ <td><%= format_rm_datetime e[:datetime] %></td>
20
+ <td><%= e[:tag_name] %></td>
21
+ <td><%= e[:namespace_name] %></td>
22
+ <td><%= status_tag e[:status] %></td>
23
+ <td class="nowrap"><%= ms e[:duration] %></td>
24
+ </tr>
25
+ <% end %>
26
+ </tbody>
27
+ </table>
@@ -0,0 +1,52 @@
1
+ <table class="<%= table.table_classes %>">
2
+ <thead>
3
+ <tr>
4
+ <th data-sort="string">Datetime</th>
5
+ <th data-sort="string">Job ID</th>
6
+ <th data-sort="string">Type</th>
7
+ <th data-sort="string">Class</th>
8
+ <th data-sort="string">Method</th>
9
+ <th data-sort="string">Status</th>
10
+ <th data-sort="float">Duration</th>
11
+ <th>Args</th>
12
+ <th>Error</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% if table.data.empty? %>
17
+ <tr>
18
+ <td colspan="9"><%= table.empty_message %></td>
19
+ </tr>
20
+ <% end %>
21
+ <% table.data.each do |e| %>
22
+ <tr>
23
+ <td><%= format_rm_datetime e[:datetime] %></td>
24
+ <td><%= e[:jid] %></td>
25
+ <td><%= e[:source_type] %></td>
26
+ <td><%= e[:class_name] %></td>
27
+ <td><%= e[:method_name] %></td>
28
+ <td><%= status_tag e[:status] %></td>
29
+ <td class="nowrap"><%= ms e[:duration], 1 %></td>
30
+ <td class="very-small-text">
31
+ <% args = e[:job_args] %>
32
+ <% if args.present? %>
33
+ <%= args.is_a?(Array) ? args.map(&:inspect).join(', ') : args.inspect %>
34
+ <% else %>
35
+ -
36
+ <% end %>
37
+ </td>
38
+ <td>
39
+ <% if e[:error_message].present? %>
40
+ <div><%= e[:error_message] %></div>
41
+ <% end %>
42
+ <% if e[:error_backtrace].present? %>
43
+ <pre class="rm-backtrace rm-backtrace-inline"><%= e[:error_backtrace] %></pre>
44
+ <% end %>
45
+ <% if e[:error_message].blank? && e[:error_backtrace].blank? %>
46
+ -
47
+ <% end %>
48
+ </td>
49
+ </tr>
50
+ <% end %>
51
+ </tbody>
52
+ </table>
@@ -0,0 +1,4 @@
1
+ <%= link_to request.original_url + '.csv', class: 'rm-icon-button rm-icon-button--ghost download_icon', title: 'Export CSV' do %>
2
+ <%= icon('download') %>
3
+ <span>Export</span>
4
+ <% end %>
@@ -0,0 +1,31 @@
1
+ <table class="<%= table.table_classes %>">
2
+ <thead>
3
+ <tr>
4
+ <th data-sort="string">Datetime</th>
5
+ <th data-sort="string">Method</th>
6
+ <th data-sort="string">Path</th>
7
+ <th data-sort="string">Status</th>
8
+ <th data-sort="float">endpoint_render.grape</th>
9
+ <th data-sort="float">format_response.grape</th>
10
+ <th data-sort="float">endpoint_run.grape</th>
11
+ </tr>
12
+ </thead>
13
+ <tbody>
14
+ <% if table.data.empty? %>
15
+ <tr>
16
+ <td colspan="7"><%= table.empty_message %></td>
17
+ </tr>
18
+ <% end %>
19
+ <% table.data.each do |e| %>
20
+ <tr>
21
+ <td><%= format_rm_datetime e[:datetime] %></td>
22
+ <td><%= e[:method] %></td>
23
+ <td><%= e[:path] %></td>
24
+ <td><%= status_tag e[:status] %></td>
25
+ <td class="nowrap"><%= ms e["endpoint_render.grape"] %></td>
26
+ <td class="nowrap"><%= ms e["format_response.grape"] %></td>
27
+ <td class="nowrap"><%= ms e["endpoint_run.grape"] %></td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>