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,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,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>
|