Module: Railswatch::RailswatchHelper

Defined in:
app/helpers/railswatch/railswatch_helper.rb

Constant Summary collapse

[
  { section: :dashboard, label: 'Overview', route: :railswatch_url },
  { section: :requests, label: 'Requests', route: :railswatch_requests_url },
  { section: :recent, label: 'Recent', route: :railswatch_recent_url },
  { section: :slow, label: 'Slow', route: :railswatch_slow_url },
  { section: :crashes, label: 'Errors', route: :railswatch_crashes_url },
  {
    section: :resources,
    label: 'System',
    route: :railswatch_resources_url,
    visible: -> { Railswatch._resource_monitor_enabled }
  },
  {
    section: :sidekiq,
    label: 'Sidekiq',
    route: :railswatch_sidekiq_url,
    visible: -> { defined?(Sidekiq) }
  },
  {
    section: :delayed_job,
    label: 'Delayed Job',
    route: :railswatch_delayed_job_url,
    visible: -> { defined?(Delayed::Job) }
  },
  {
    section: :grape,
    label: 'Grape',
    route: :railswatch_grape_url,
    visible: -> { defined?(Grape) }
  },
  {
    section: :rake,
    label: 'Rake',
    route: :railswatch_rake_url,
    visible: -> { Railswatch.include_rake_tasks }
  },
  {
    section: :custom,
    label: 'Custom',
    route: :railswatch_custom_url,
    visible: -> { Railswatch.include_custom_events }
  }
].freeze
PAGE_META =
{
  index: {
    eyebrow: 'Performance',
    title: 'Application Command Center',
    description: lambda {
      "Live request health, latency, and throughput over the last #{human_window(Railswatch.duration)}."
    },
    badge: 'Live'
  },
  requests: {
    eyebrow: 'Analysis',
    title: 'Request Breakdown',
    description: 'Compare controllers and actions by volume, percentiles, averages, and slowest paths.'
  },
  recent: {
    eyebrow: 'Realtime',
    title: 'Recent Requests',
    description: lambda {
      "Inspect fresh traffic sampled from the last #{human_window(Railswatch.recent_requests_time_window)}."
    }
  },
  slow: {
    eyebrow: 'Latency',
    title: 'Slow Requests',
    description: lambda {
      "Focus on requests above #{Railswatch.slow_requests_threshold} ms " \
        'and inspect traces before they age out.'
    }
  },
  crashes: {
    eyebrow: 'Reliability',
    title: '500 Error Timeline',
    description: 'Review failures, backtraces, and the request context that led to them.'
  },
  resources: {
    eyebrow: 'Infrastructure',
    title: 'System Resources',
    description: lambda {
      'Watch CPU, memory, and disk behavior across the last ' \
        "#{human_window(Railswatch.system_monitor_duration)}."
    }
  },
  sidekiq: {
    eyebrow: 'Async Jobs',
    title: 'Sidekiq Workload',
    description: 'Track job throughput, worker runtime, and recent executions.'
  },
  delayed_job: {
    eyebrow: 'Async Jobs',
    title: 'Delayed Job Workload',
    description: 'Monitor queue throughput, execution time, and recent jobs.'
  },
  grape: {
    eyebrow: 'API',
    title: 'Grape Endpoints',
    description: 'Keep an eye on API traffic volume and recent endpoint activity.'
  },
  rake: {
    eyebrow: 'Automation',
    title: 'Rake Tasks',
    description: 'Surface task runtime and throughput for your scheduled or manual jobs.'
  },
  custom: {
    eyebrow: 'Business Events',
    title: 'Custom Measurements',
    description: 'Explore custom event timing and throughput captured with Railswatch.measure.'
  },
  default: {
    eyebrow: 'Monitoring',
    title: 'Railswatch',
    description: 'Observe the health of your Rails application.'
  }
}.freeze

Instance Method Summary collapse

Instance Method Details

#active?(section) ⇒ Boolean

Returns:

  • (Boolean)


297
298
299
300
301
302
303
304
305
306
307
308
# File 'app/helpers/railswatch/railswatch_helper.rb', line 297

def active?(section)
  actions = {
    dashboard: 'index', crashes: 'crashes',
    requests: 'requests', resources: 'resources',
    recent: 'recent', slow: 'slow',
    sidekiq: 'sidekiq', delayed_job: 'delayed_job',
    grape: 'grape', rake: 'rake', custom: 'custom'
  }
  return false unless controller_name == 'railswatch'

  'is-active' if action_name == actions[section]
end

#bot_icon(user_agent) ⇒ Object

rubocop:disable Metrics/MethodLength



262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'app/helpers/railswatch/railswatch_helper.rb', line 262

def bot_icon(user_agent) # rubocop:disable Metrics/MethodLength
  return nil if user_agent.blank?

  browser = Browser.new(user_agent)
  if browser.bot?
    (:span, class: 'user-agent-icon', title: browser.bot&.name) do
      icon('bot')
    end
  else
    (:span, class: 'user-agent-icon user-agent-icon-user', title: 'Real User') do
      icon('user')
    end
  end
end

#card_caption(label) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
# File 'app/helpers/railswatch/railswatch_helper.rb', line 205

def card_caption(label)
  case label.to_s.downcase
  when 'p50'
    'Median request latency'
  when 'p95'
    'Tail latency for slower traffic'
  when 'p99'
    'Worst-case request experience'
  else
    'Request insight'
  end
end

#card_tone(label) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
# File 'app/helpers/railswatch/railswatch_helper.rb', line 192

def card_tone(label)
  case label.to_s.downcase
  when 'p50'
    'healthy'
  when 'p95'
    'warning'
  when 'p99'
    'critical'
  else
    'neutral'
  end
end

#compact_number(value) ⇒ Object



174
175
176
177
178
# File 'app/helpers/railswatch/railswatch_helper.rb', line 174

def compact_number(value)
  return '0' if value.blank?

  number_to_human(value, precision: 3, strip_insignificant_zeros: true)
end

#duration_alert_class(duration_str) ⇒ Object

rubocop:disable Metrics/MethodLength



142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'app/helpers/railswatch/railswatch_helper.rb', line 142

def duration_alert_class(duration_str) # rubocop:disable Metrics/MethodLength
  if duration_str.to_s =~ /(\d+.?\d+?)/
    duration = ::Regexp.last_match(1).to_f
    if duration >= 500
      'has-background-danger has-text-white-bis'
    elsif duration >= 200
      'has-background-warning has-text-black-ter'
    else
      'has-background-success has-text-white-bis'
    end
  else
    'has-background-light'
  end
end

#extract_duration(str) ⇒ Object



157
158
159
160
161
162
163
164
165
# File 'app/helpers/railswatch/railswatch_helper.rb', line 157

def extract_duration(str)
  return str[:duration].to_s if str.is_a?(Hash) && str[:duration]

  if str =~ /Duration: (\d+.?\d+?ms)/i
    ::Regexp.last_match(1)
  else
    '-'
  end
end

#format_datetime(event = nil) ⇒ Object

Keep this method permissive because host applications may already call ‘format_datetime` with arbitrary values in their own templates.



291
292
293
294
295
# File 'app/helpers/railswatch/railswatch_helper.rb', line 291

def format_datetime(event = nil, **)
  return event.to_s unless event.respond_to?(:in_time_zone) || event.respond_to?(:utc)

  format_rm_datetime(event)
end

#format_rm_datetime(event) ⇒ Object



284
285
286
287
# File 'app/helpers/railswatch/railswatch_helper.rb', line 284

def format_rm_datetime(event)
  dt = Railswatch::Reports::BaseReport.time_in_app_time_zone(event)
  I18n.l(dt, format: '%Y-%m-%d %H:%M:%S')
end

#health_tone(value) ⇒ Object



184
185
186
187
188
189
190
# File 'app/helpers/railswatch/railswatch_helper.rb', line 184

def health_tone(value)
  return 'neutral' if value.nil?
  return 'critical' if value >= 5
  return 'warning' if value >= 1

  'healthy'
end

#icon(name) ⇒ Object



277
278
279
280
281
282
# File 'app/helpers/railswatch/railswatch_helper.rb', line 277

def icon(name)
  @icons ||= {}

  # https://www.iconfinder.com/iconsets/vivid
  @icons[name] ||= raw File.read(File.expand_path(File.dirname(__FILE__) + "/../../assets/images/#{name}.svg"))
end


224
225
226
227
228
229
230
# File 'app/helpers/railswatch/railswatch_helper.rb', line 224

def link_to_path(event)
  if event[:method] == 'GET'
    link_to(short_path(event[:path]), event[:path], target: '_blank', title: short_path(event[:path]))
  else
    short_path(event[:path])
  end
end

#ms(value, limit = 1) ⇒ Object



167
168
169
170
171
172
# File 'app/helpers/railswatch/railswatch_helper.rb', line 167

def ms(value, limit = 1)
  result = round_it(value, limit)
  return '-' if result.nil?

  result && result != 0 ? "#{result} ms" : '< 0 ms'
end


123
124
125
126
127
128
129
# File 'app/helpers/railswatch/railswatch_helper.rb', line 123

def navigation_items
  NAVIGATION_ITEMS.filter_map do |item|
    next unless navigation_item_visible?(item)

    item.slice(:section, :label).merge(path: railswatch.public_send(item[:route]))
  end
end

#page_meta(section = action_name.to_sym) ⇒ Object



131
132
133
# File 'app/helpers/railswatch/railswatch_helper.rb', line 131

def page_meta(section = action_name.to_sym)
  resolve_page_meta(PAGE_META[section.to_sym] || PAGE_META[:default])
end

#percentage(value, precision = 1) ⇒ Object



180
181
182
# File 'app/helpers/railswatch/railswatch_helper.rb', line 180

def percentage(value, precision = 1)
  number_to_percentage(value.to_f, precision: precision)
end

#report_name(hash) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'app/helpers/railswatch/railswatch_helper.rb', line 232

def report_name(hash)
  hash.except(:on).collect do |key, value|
    next if value.blank?

    %(
    <div class="control">
      <span class="tags has-addons">
        <span class="tag">#{key}</span>
        <span class="tag is-info is-light">#{value}</span>
      </span>
    </div>)
  end.compact.join.html_safe
end

#round_it(value, limit = 1) ⇒ Object



135
136
137
138
139
140
# File 'app/helpers/railswatch/railswatch_helper.rb', line 135

def round_it(value, limit = 1)
  return nil unless value
  return value if value.is_a?(Integer)

  value.nan? ? nil : value.round(limit)
end

#short_path(path, length: 55) ⇒ Object



218
219
220
221
222
# File 'app/helpers/railswatch/railswatch_helper.rb', line 218

def short_path(path, length: 55)
   :span, title: path do
    truncate(path, length: length)
  end
end

#status_tag(status) ⇒ Object

rubocop:disable Metrics/MethodLength



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'app/helpers/railswatch/railswatch_helper.rb', line 246

def status_tag(status) # rubocop:disable Metrics/MethodLength
  klass = case status.to_s
          when /error/, /^5/
            'tag is-danger'
          when /^4/
            'tag is-warning'
          when /^3/
            'tag is-info'
          when /^2/, /success/
            'tag is-success'
          end
  (:span, class: klass) do
    status
  end
end