flare 0.1.1 → 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.
Potentially problematic release.
This version of flare might be problematic. Click here for more details.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/flare.gemspec +66 -0
- data/lib/flare/active_record.rb +102 -0
- data/lib/flare/collection.rb +47 -0
- data/lib/flare/configuration.rb +64 -109
- data/lib/flare/index_builder.rb +24 -0
- data/lib/flare/session.rb +142 -0
- data/lib/flare/tasks.rb +18 -0
- data/lib/flare.rb +19 -408
- data/test/helper.rb +10 -0
- data/test/test_flare.rb +7 -0
- metadata +89 -228
- checksums.yaml +0 -7
- data/CHANGELOG.md +0 -5
- data/LICENSE.txt +0 -21
- data/README.md +0 -148
- data/app/controllers/flare/application_controller.rb +0 -22
- data/app/controllers/flare/jobs_controller.rb +0 -55
- data/app/controllers/flare/requests_controller.rb +0 -73
- data/app/controllers/flare/spans_controller.rb +0 -101
- data/app/helpers/flare/application_helper.rb +0 -168
- data/app/views/flare/jobs/index.html.erb +0 -69
- data/app/views/flare/jobs/show.html.erb +0 -323
- data/app/views/flare/requests/index.html.erb +0 -120
- data/app/views/flare/requests/show.html.erb +0 -498
- data/app/views/flare/spans/index.html.erb +0 -112
- data/app/views/flare/spans/show.html.erb +0 -184
- data/app/views/layouts/flare/application.html.erb +0 -126
- data/config/routes.rb +0 -20
- data/exe/flare +0 -9
- data/lib/flare/backoff_policy.rb +0 -73
- data/lib/flare/cli/doctor_command.rb +0 -129
- data/lib/flare/cli/output.rb +0 -45
- data/lib/flare/cli/setup_command.rb +0 -404
- data/lib/flare/cli/status_command.rb +0 -47
- data/lib/flare/cli.rb +0 -50
- data/lib/flare/engine.rb +0 -43
- data/lib/flare/http_metrics_config.rb +0 -101
- data/lib/flare/metric_counter.rb +0 -45
- data/lib/flare/metric_flusher.rb +0 -124
- data/lib/flare/metric_key.rb +0 -42
- data/lib/flare/metric_span_processor.rb +0 -470
- data/lib/flare/metric_storage.rb +0 -42
- data/lib/flare/metric_submitter.rb +0 -221
- data/lib/flare/source_location.rb +0 -113
- data/lib/flare/sqlite_exporter.rb +0 -279
- data/lib/flare/storage/sqlite.rb +0 -789
- data/lib/flare/storage.rb +0 -54
- data/lib/flare/version.rb +0 -5
- data/public/flare-assets/flare.css +0 -1245
- data/public/flare-assets/images/flipper.png +0 -0
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Flare
|
|
4
|
-
class SpansController < ApplicationController
|
|
5
|
-
around_action :untrace_request
|
|
6
|
-
|
|
7
|
-
helper_method :current_section, :page_title, :category_config
|
|
8
|
-
|
|
9
|
-
PER_PAGE = 50
|
|
10
|
-
|
|
11
|
-
CATEGORIES = {
|
|
12
|
-
"queries" => { title: "Queries", icon: "database", badge_class: "sql" },
|
|
13
|
-
"cache" => { title: "Cache", icon: "archive", badge_class: "cache" },
|
|
14
|
-
"views" => { title: "Views", icon: "layout", badge_class: "view" },
|
|
15
|
-
"http" => { title: "HTTP", icon: "globe", badge_class: "http" },
|
|
16
|
-
"mail" => { title: "Mail", icon: "mail", badge_class: "mail" },
|
|
17
|
-
"redis" => { title: "Redis", icon: "database", badge_class: "other" },
|
|
18
|
-
"exceptions" => { title: "Exceptions", icon: "alert-triangle", badge_class: "exception" }
|
|
19
|
-
}.freeze
|
|
20
|
-
|
|
21
|
-
def queries
|
|
22
|
-
list_spans("queries")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def cache
|
|
26
|
-
list_spans("cache")
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def views
|
|
30
|
-
list_spans("views")
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def http
|
|
34
|
-
list_spans("http")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def mail
|
|
38
|
-
list_spans("mail")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def redis
|
|
42
|
-
list_spans("redis")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def exceptions
|
|
46
|
-
list_spans("exceptions")
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def show
|
|
50
|
-
@span = Flare.storage.find_span(params[:id])
|
|
51
|
-
|
|
52
|
-
if @span.blank?
|
|
53
|
-
redirect_to requests_path, alert: "Span not found"
|
|
54
|
-
return
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
@category = params[:category]
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def list_spans(category)
|
|
63
|
-
@category = category
|
|
64
|
-
@offset = params[:offset].to_i
|
|
65
|
-
filter_params = { name: params[:name].presence }
|
|
66
|
-
|
|
67
|
-
spans = Flare.storage.list_spans_by_category(category, **filter_params, limit: PER_PAGE + 1, offset: @offset)
|
|
68
|
-
@total_count = Flare.storage.count_spans_by_category(category, **filter_params)
|
|
69
|
-
@has_next = spans.size > PER_PAGE
|
|
70
|
-
@spans = spans.first(PER_PAGE)
|
|
71
|
-
@has_prev = @offset > 0
|
|
72
|
-
|
|
73
|
-
# Load properties for display
|
|
74
|
-
span_ids = @spans.map { |s| s[:id] }
|
|
75
|
-
if span_ids.any?
|
|
76
|
-
all_properties = Flare.storage.load_properties_for_ids("Flare::Span", span_ids)
|
|
77
|
-
@spans.each do |span|
|
|
78
|
-
span[:properties] = all_properties[span[:id]] || {}
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
render :index
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def untrace_request
|
|
86
|
-
Flare.untraced { yield }
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def current_section
|
|
90
|
-
@category || "spans"
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def page_title
|
|
94
|
-
category_config[:title]
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def category_config
|
|
98
|
-
CATEGORIES[@category] || { title: "Spans", icon: "activity", badge_class: "other" }
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Flare
|
|
4
|
-
module ApplicationHelper
|
|
5
|
-
def span_category(span)
|
|
6
|
-
name = span[:name].to_s.downcase
|
|
7
|
-
case name
|
|
8
|
-
when /sql\.active_record/, /mysql/, /postgres/, /sqlite/
|
|
9
|
-
"sql"
|
|
10
|
-
when /cache/
|
|
11
|
-
"cache"
|
|
12
|
-
when /render/, /view/
|
|
13
|
-
"view"
|
|
14
|
-
when /http/, /net_http/, /faraday/
|
|
15
|
-
"http"
|
|
16
|
-
when /mail/
|
|
17
|
-
"mailer"
|
|
18
|
-
when /job/, /active_job/
|
|
19
|
-
"job"
|
|
20
|
-
when /action_controller/, /process_action/
|
|
21
|
-
"controller"
|
|
22
|
-
else
|
|
23
|
-
"other"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def span_display_info(span, category)
|
|
28
|
-
props = span[:properties] || {}
|
|
29
|
-
case category
|
|
30
|
-
when "queries"
|
|
31
|
-
stmt = props["db.statement"]&.to_s
|
|
32
|
-
name = props["name"]&.to_s
|
|
33
|
-
db_name = props["db.name"]&.to_s
|
|
34
|
-
source_loc = if props["code.filepath"] && props["code.lineno"]
|
|
35
|
-
"#{props["code.filepath"]}:#{props["code.lineno"]}"
|
|
36
|
-
end
|
|
37
|
-
secondary = [name.presence, db_name.presence, source_loc].compact.join(" \u00b7 ").presence
|
|
38
|
-
if stmt.present?
|
|
39
|
-
{ primary: stmt, secondary: secondary }
|
|
40
|
-
elsif name.present?
|
|
41
|
-
{ primary: name, secondary: [db_name.presence, source_loc].compact.join(" \u00b7 ").presence }
|
|
42
|
-
else
|
|
43
|
-
{ primary: span[:name], secondary: [db_name.presence, source_loc].compact.join(" \u00b7 ").presence }
|
|
44
|
-
end
|
|
45
|
-
when "cache"
|
|
46
|
-
key = props["key"]&.to_s
|
|
47
|
-
op = span[:name].to_s.sub(".active_support", "").sub("cache_", "")
|
|
48
|
-
store = props["store"]&.to_s&.sub(/^ActiveSupport::Cache::/, "")
|
|
49
|
-
{ primary: key.presence || span[:name], secondary: store, cache_op: op }
|
|
50
|
-
when "views"
|
|
51
|
-
identifier = props["identifier"] || props["code.filepath"]
|
|
52
|
-
primary = identifier ? identifier.to_s.sub(/^.*\/app\/views\//, "") : span[:name]
|
|
53
|
-
{ primary: primary, secondary: nil }
|
|
54
|
-
when "http"
|
|
55
|
-
full_url = props["http.url"] || ""
|
|
56
|
-
target = props["http.target"] || ""
|
|
57
|
-
host = props["http.host"] || props["net.peer.name"] || props["peer.service"]
|
|
58
|
-
uri = URI.parse(full_url) rescue nil
|
|
59
|
-
if uri && uri.host
|
|
60
|
-
domain = uri.host
|
|
61
|
-
path = uri.path.presence || "/"
|
|
62
|
-
path = "#{path}?#{uri.query}" if uri.query.present?
|
|
63
|
-
else
|
|
64
|
-
domain = host
|
|
65
|
-
path = target.presence || full_url
|
|
66
|
-
end
|
|
67
|
-
method = props["http.method"]
|
|
68
|
-
status = props["http.status_code"]
|
|
69
|
-
{ primary: path.to_s.truncate(100), secondary: domain, http_method: method, http_status: status }
|
|
70
|
-
when "mail"
|
|
71
|
-
mailer = props["mailer"]
|
|
72
|
-
action = props["action"]
|
|
73
|
-
subject = props["subject"]
|
|
74
|
-
if mailer && action
|
|
75
|
-
{ primary: "#{mailer}##{action}", secondary: subject }
|
|
76
|
-
else
|
|
77
|
-
{ primary: span[:name], secondary: nil }
|
|
78
|
-
end
|
|
79
|
-
when "redis"
|
|
80
|
-
cmd = props["db.statement"]&.to_s
|
|
81
|
-
{ primary: cmd.presence || span[:name], secondary: nil }
|
|
82
|
-
when "exceptions"
|
|
83
|
-
exc_type = span[:exception_type]
|
|
84
|
-
exc_message = span[:exception_message]
|
|
85
|
-
primary = if exc_type.present? && exc_message.present?
|
|
86
|
-
"#{exc_type}: #{exc_message}"
|
|
87
|
-
elsif exc_message.present?
|
|
88
|
-
exc_message
|
|
89
|
-
elsif exc_type.present?
|
|
90
|
-
exc_type
|
|
91
|
-
else
|
|
92
|
-
span[:name]
|
|
93
|
-
end
|
|
94
|
-
stacktrace = span[:exception_stacktrace].to_s
|
|
95
|
-
first_app_line = stacktrace.split("\n").find { |line| line.include?("/app/") } || stacktrace.split("\n").first
|
|
96
|
-
secondary = first_app_line&.strip.to_s.truncate(200)
|
|
97
|
-
{ primary: primary, secondary: secondary }
|
|
98
|
-
else
|
|
99
|
-
{ primary: span[:name], secondary: nil }
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def format_duration(ms)
|
|
104
|
-
return "-" if ms.nil?
|
|
105
|
-
|
|
106
|
-
if ms >= 1000
|
|
107
|
-
"#{(ms / 1000.0).round(1)}s"
|
|
108
|
-
else
|
|
109
|
-
"#{ms.round(1)}ms"
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def format_content(data, indent = 0)
|
|
114
|
-
return "" if data.nil?
|
|
115
|
-
|
|
116
|
-
lines = []
|
|
117
|
-
prefix = " " * indent
|
|
118
|
-
|
|
119
|
-
case data
|
|
120
|
-
when Hash
|
|
121
|
-
data.each do |key, value|
|
|
122
|
-
if value.is_a?(Hash) || value.is_a?(Array)
|
|
123
|
-
lines << "#{prefix}#{key}:"
|
|
124
|
-
lines << format_content(value, indent + 1)
|
|
125
|
-
else
|
|
126
|
-
formatted_value = format_value(value)
|
|
127
|
-
if formatted_value.include?("\n")
|
|
128
|
-
lines << "#{prefix}#{key}:"
|
|
129
|
-
formatted_value.each_line do |line|
|
|
130
|
-
lines << "#{prefix} #{line.rstrip}"
|
|
131
|
-
end
|
|
132
|
-
else
|
|
133
|
-
lines << "#{prefix}#{key}: #{formatted_value}"
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
when Array
|
|
138
|
-
data.each do |item|
|
|
139
|
-
if item.is_a?(Hash) || item.is_a?(Array)
|
|
140
|
-
lines << "#{prefix}-"
|
|
141
|
-
lines << format_content(item, indent + 1)
|
|
142
|
-
else
|
|
143
|
-
lines << "#{prefix}- #{format_value(item)}"
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
else
|
|
147
|
-
lines << "#{prefix}#{format_value(data)}"
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
lines.join("\n")
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
private
|
|
154
|
-
|
|
155
|
-
def format_value(value)
|
|
156
|
-
case value
|
|
157
|
-
when nil
|
|
158
|
-
"null"
|
|
159
|
-
when true, false
|
|
160
|
-
value.to_s
|
|
161
|
-
when Numeric
|
|
162
|
-
value.to_s
|
|
163
|
-
else
|
|
164
|
-
value.to_s
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
<div class="page-header">
|
|
2
|
-
<h1 class="page-title"><%= page_title %></h1>
|
|
3
|
-
<div class="filters">
|
|
4
|
-
<%= form_tag(jobs_path, method: :get, id: "filter-form") do %>
|
|
5
|
-
<%= text_field_tag :name, params[:name], placeholder: "Search...", class: "filter-select", style: "min-width: 200px;", onchange: "this.form.submit()" %>
|
|
6
|
-
<% end %>
|
|
7
|
-
</div>
|
|
8
|
-
</div>
|
|
9
|
-
|
|
10
|
-
<div class="page-body">
|
|
11
|
-
<div class="card">
|
|
12
|
-
<div class="card-body">
|
|
13
|
-
<% if @jobs.blank? %>
|
|
14
|
-
<div class="empty-state">
|
|
15
|
-
<div class="empty-state-icon">
|
|
16
|
-
<i class="bi bi-briefcase" style="font-size: 1.5rem;"></i>
|
|
17
|
-
</div>
|
|
18
|
-
<h3>No jobs found</h3>
|
|
19
|
-
<p>Data will appear here as your application processes background jobs.</p>
|
|
20
|
-
</div>
|
|
21
|
-
<% else %>
|
|
22
|
-
<table class="data-table">
|
|
23
|
-
<thead>
|
|
24
|
-
<tr>
|
|
25
|
-
<th>Job</th>
|
|
26
|
-
<th style="width: 100px; text-align: right;">Duration</th>
|
|
27
|
-
<th style="width: 120px; text-align: right;">Happened</th>
|
|
28
|
-
</tr>
|
|
29
|
-
</thead>
|
|
30
|
-
<tbody>
|
|
31
|
-
<% @jobs.each do |job| %>
|
|
32
|
-
<% started_at = Time.at(job[:start_timestamp] / 1_000_000_000.0) rescue nil %>
|
|
33
|
-
<%
|
|
34
|
-
display_name = job[:job_class] || job[:name].to_s.sub(/ (process|publish)$/, '')
|
|
35
|
-
%>
|
|
36
|
-
<tr onclick="navigateIfNotSelecting('<%= job_path(job[:trace_id]) %>')" style="cursor: pointer;">
|
|
37
|
-
<td class="path-cell">
|
|
38
|
-
<span class="job-name path-text" title="<%= display_name %>"><%= display_name %></span>
|
|
39
|
-
<% queue = job[:queue_name] || "default" %>
|
|
40
|
-
<span class="path-subtext" title="<%= queue %>"><%= queue %></span>
|
|
41
|
-
</td>
|
|
42
|
-
<td class="duration" style="text-align: right;"><%= format_duration(job[:duration_ms]) %></td>
|
|
43
|
-
<td class="timestamp" style="text-align: right;"><%= started_at ? time_ago_in_words(started_at) : "-" %></td>
|
|
44
|
-
</tr>
|
|
45
|
-
<% end %>
|
|
46
|
-
</tbody>
|
|
47
|
-
</table>
|
|
48
|
-
|
|
49
|
-
<% if @total_count > 0 %>
|
|
50
|
-
<div class="pagination">
|
|
51
|
-
<div class="pagination-side">
|
|
52
|
-
<% if @has_prev %>
|
|
53
|
-
<%= link_to "Previous", jobs_path(request.query_parameters.merge(offset: [@offset - 50, 0].max)), class: "pagination-link" %>
|
|
54
|
-
<% end %>
|
|
55
|
-
</div>
|
|
56
|
-
<div class="pagination-info">
|
|
57
|
-
<%= number_with_delimiter(@offset + 1) %> - <%= number_with_delimiter([@offset + @jobs.size, @total_count].min) %> of <%= number_with_delimiter(@total_count) %>
|
|
58
|
-
</div>
|
|
59
|
-
<div class="pagination-side" style="text-align: right;">
|
|
60
|
-
<% if @has_next %>
|
|
61
|
-
<%= link_to "Next", jobs_path(request.query_parameters.merge(offset: @offset + 50)), class: "pagination-link" %>
|
|
62
|
-
<% end %>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
<% end %>
|
|
66
|
-
<% end %>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
<%
|
|
2
|
-
# Get root span properties
|
|
3
|
-
root_props = @root_span ? @root_span[:properties] : {}
|
|
4
|
-
job_class = root_props["code.namespace"]
|
|
5
|
-
queue_name = root_props["messaging.destination"]
|
|
6
|
-
job_id = root_props["messaging.message.id"] || root_props["messaging.message_id"]
|
|
7
|
-
|
|
8
|
-
# Calculate total duration from root span
|
|
9
|
-
total_duration_ms = @root_span ? @root_span[:duration_ms] : 1
|
|
10
|
-
root_start = @root_span ? @root_span[:start_timestamp] : 0
|
|
11
|
-
started_at = @root_span ? Time.at(@root_span[:start_timestamp] / 1_000_000_000.0) : nil
|
|
12
|
-
|
|
13
|
-
# Extract job name from span name
|
|
14
|
-
job_display_name = job_class || @job[:name].to_s.sub(/ (process|publish)$/, '')
|
|
15
|
-
|
|
16
|
-
# Count span categories
|
|
17
|
-
span_counts = @child_spans.each_with_object(Hash.new(0)) do |span, counts|
|
|
18
|
-
counts[span_category(span)] += 1
|
|
19
|
-
end
|
|
20
|
-
%>
|
|
21
|
-
|
|
22
|
-
<div class="page-header">
|
|
23
|
-
<div class="detail-header">
|
|
24
|
-
<div class="detail-header-left">
|
|
25
|
-
<div class="detail-title">
|
|
26
|
-
<span class="badge badge-job">Job</span>
|
|
27
|
-
<h1><%= job_display_name %></h1>
|
|
28
|
-
</div>
|
|
29
|
-
<div class="detail-meta">
|
|
30
|
-
<div class="meta-item">
|
|
31
|
-
<span class="meta-label">Duration</span>
|
|
32
|
-
<span class="meta-value mono"><%= format_duration(total_duration_ms) %></span>
|
|
33
|
-
</div>
|
|
34
|
-
<div class="meta-item">
|
|
35
|
-
<span class="meta-label">Time</span>
|
|
36
|
-
<span class="meta-value"><%= started_at&.strftime("%b %d, %Y %H:%M:%S") %></span>
|
|
37
|
-
</div>
|
|
38
|
-
<% if queue_name %>
|
|
39
|
-
<div class="meta-item">
|
|
40
|
-
<span class="meta-label">Queue</span>
|
|
41
|
-
<span class="meta-value"><%= queue_name %></span>
|
|
42
|
-
</div>
|
|
43
|
-
<% end %>
|
|
44
|
-
<% if job_id %>
|
|
45
|
-
<div class="meta-item">
|
|
46
|
-
<span class="meta-label">Job ID</span>
|
|
47
|
-
<span class="meta-value mono"><%= job_id %></span>
|
|
48
|
-
</div>
|
|
49
|
-
<% end %>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<div class="page-body">
|
|
56
|
-
<div class="card">
|
|
57
|
-
<div class="tabs">
|
|
58
|
-
<a class="tab active" onclick="showTab('timeline')">Timeline (<%= @child_spans.size %>)</a>
|
|
59
|
-
<a class="tab" onclick="showTab('details')">Details</a>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<div id="tab-details" class="tab-content">
|
|
63
|
-
<%
|
|
64
|
-
shown_keys = %w[code.namespace messaging.destination messaging.message.id messaging.message_id]
|
|
65
|
-
other_props = root_props.reject { |k, v| shown_keys.include?(k) || v.is_a?(Hash) || v.is_a?(Array) }
|
|
66
|
-
%>
|
|
67
|
-
<div class="details-sections">
|
|
68
|
-
<!-- Job Section -->
|
|
69
|
-
<div class="details-section">
|
|
70
|
-
<div class="section-header">
|
|
71
|
-
<div class="section-icon job">
|
|
72
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
73
|
-
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect>
|
|
74
|
-
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path>
|
|
75
|
-
</svg>
|
|
76
|
-
</div>
|
|
77
|
-
<span class="section-title">Job</span>
|
|
78
|
-
</div>
|
|
79
|
-
<div class="section-grid">
|
|
80
|
-
<div class="detail-item">
|
|
81
|
-
<span class="detail-label">Duration</span>
|
|
82
|
-
<span class="detail-value prominent"><%= format_duration(total_duration_ms) %></span>
|
|
83
|
-
</div>
|
|
84
|
-
<div class="detail-item">
|
|
85
|
-
<span class="detail-label">Time</span>
|
|
86
|
-
<span class="detail-value"><%= started_at&.strftime("%b %d, %Y at %H:%M:%S") %></span>
|
|
87
|
-
</div>
|
|
88
|
-
<% if job_class %>
|
|
89
|
-
<div class="detail-item">
|
|
90
|
-
<span class="detail-label">Job Class</span>
|
|
91
|
-
<span class="detail-value mono"><%= job_class %></span>
|
|
92
|
-
</div>
|
|
93
|
-
<% end %>
|
|
94
|
-
<% if job_id %>
|
|
95
|
-
<div class="detail-item full-width">
|
|
96
|
-
<span class="detail-label">Job ID</span>
|
|
97
|
-
<span class="detail-value mono secondary"><%= job_id %></span>
|
|
98
|
-
</div>
|
|
99
|
-
<% end %>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<!-- Queue Section -->
|
|
104
|
-
<% if queue_name %>
|
|
105
|
-
<div class="details-section">
|
|
106
|
-
<div class="section-header">
|
|
107
|
-
<div class="section-icon queue">
|
|
108
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
109
|
-
<line x1="8" y1="6" x2="21" y2="6"></line>
|
|
110
|
-
<line x1="8" y1="12" x2="21" y2="12"></line>
|
|
111
|
-
<line x1="8" y1="18" x2="21" y2="18"></line>
|
|
112
|
-
<line x1="3" y1="6" x2="3.01" y2="6"></line>
|
|
113
|
-
<line x1="3" y1="12" x2="3.01" y2="12"></line>
|
|
114
|
-
<line x1="3" y1="18" x2="3.01" y2="18"></line>
|
|
115
|
-
</svg>
|
|
116
|
-
</div>
|
|
117
|
-
<span class="section-title">Queue</span>
|
|
118
|
-
</div>
|
|
119
|
-
<div class="section-grid">
|
|
120
|
-
<div class="detail-item">
|
|
121
|
-
<span class="detail-label">Queue Name</span>
|
|
122
|
-
<span class="detail-value mono"><%= queue_name %></span>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
<% end %>
|
|
127
|
-
|
|
128
|
-
<!-- Other Properties Section -->
|
|
129
|
-
<% if other_props.any? %>
|
|
130
|
-
<div class="details-section">
|
|
131
|
-
<div class="section-header">
|
|
132
|
-
<div class="section-icon other">
|
|
133
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
134
|
-
<circle cx="12" cy="12" r="1"></circle>
|
|
135
|
-
<circle cx="19" cy="12" r="1"></circle>
|
|
136
|
-
<circle cx="5" cy="12" r="1"></circle>
|
|
137
|
-
</svg>
|
|
138
|
-
</div>
|
|
139
|
-
<span class="section-title">Additional Properties</span>
|
|
140
|
-
</div>
|
|
141
|
-
<div class="section-grid">
|
|
142
|
-
<% other_props.each do |key, value| %>
|
|
143
|
-
<div class="detail-item">
|
|
144
|
-
<span class="detail-label"><%= key.to_s.split(".").last.titleize %></span>
|
|
145
|
-
<span class="detail-value mono truncate" title="<%= value %>"><%= value %></span>
|
|
146
|
-
</div>
|
|
147
|
-
<% end %>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
<% end %>
|
|
151
|
-
|
|
152
|
-
<% if root_props.blank? %>
|
|
153
|
-
<div class="details-section">
|
|
154
|
-
<div class="empty-clues" style="padding: 1.25rem;">
|
|
155
|
-
<p>No details recorded.</p>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
<% end %>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
<div id="tab-timeline" class="tab-content active">
|
|
163
|
-
<% if @child_spans.blank? %>
|
|
164
|
-
<div class="empty-clues">
|
|
165
|
-
<i class="bi bi-clock-history" style="font-size: 2rem; color: var(--cb-warm-gray-light);"></i>
|
|
166
|
-
<p>No timeline events recorded for this job.</p>
|
|
167
|
-
</div>
|
|
168
|
-
<% else %>
|
|
169
|
-
<div class="legend">
|
|
170
|
-
<div class="legend-item clickable active" data-filter="all" onclick="filterSpans('all')"><div class="legend-color all"></div> All (<%= @child_spans.size %>)</div>
|
|
171
|
-
<% if span_counts["sql"] > 0 %><div class="legend-item clickable" data-filter="sql" onclick="filterSpans('sql')"><div class="legend-color sql"></div> SQL (<%= span_counts["sql"] %>)</div><% end %>
|
|
172
|
-
<% if span_counts["cache"] > 0 %><div class="legend-item clickable" data-filter="cache" onclick="filterSpans('cache')"><div class="legend-color cache"></div> Cache (<%= span_counts["cache"] %>)</div><% end %>
|
|
173
|
-
<% if span_counts["view"] > 0 %><div class="legend-item clickable" data-filter="view" onclick="filterSpans('view')"><div class="legend-color view"></div> View (<%= span_counts["view"] %>)</div><% end %>
|
|
174
|
-
<% if span_counts["http"] > 0 %><div class="legend-item clickable" data-filter="http" onclick="filterSpans('http')"><div class="legend-color http"></div> HTTP (<%= span_counts["http"] %>)</div><% end %>
|
|
175
|
-
<% if span_counts["mailer"] > 0 %><div class="legend-item clickable" data-filter="mailer" onclick="filterSpans('mailer')"><div class="legend-color mailer"></div> Mailer (<%= span_counts["mailer"] %>)</div><% end %>
|
|
176
|
-
<% if span_counts["job"] > 0 %><div class="legend-item clickable" data-filter="job" onclick="filterSpans('job')"><div class="legend-color job"></div> Job (<%= span_counts["job"] %>)</div><% end %>
|
|
177
|
-
<% if span_counts["controller"] > 0 %><div class="legend-item clickable" data-filter="controller" onclick="filterSpans('controller')"><div class="legend-color controller"></div> Controller (<%= span_counts["controller"] %>)</div><% end %>
|
|
178
|
-
<% if span_counts["other"] > 0 %><div class="legend-item clickable" data-filter="other" onclick="filterSpans('other')"><div class="legend-color other"></div> Other (<%= span_counts["other"] %>)</div><% end %>
|
|
179
|
-
<div style="margin-left: auto;">
|
|
180
|
-
<button type="button" onclick="collapseAll()" class="collapse-btn">
|
|
181
|
-
<i class="bi bi-arrows-collapse"></i>
|
|
182
|
-
Collapse All
|
|
183
|
-
</button>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
<div style="padding: 0.75rem;">
|
|
188
|
-
<div class="waterfall-header">
|
|
189
|
-
<div>Event</div>
|
|
190
|
-
<div>Duration</div>
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
<div class="waterfall">
|
|
194
|
-
<% @child_spans.each_with_index do |span, index| %>
|
|
195
|
-
<%
|
|
196
|
-
offset_ns = span[:start_timestamp] - root_start
|
|
197
|
-
offset_ms = offset_ns / 1_000_000.0
|
|
198
|
-
left = (offset_ms / [total_duration_ms, 1].max * 100).clamp(0, 99.5)
|
|
199
|
-
width = (span[:duration_ms] / [total_duration_ms, 1].max * 100).clamp(0.5, [0.5, 100 - left].max)
|
|
200
|
-
category = span_category(span)
|
|
201
|
-
|
|
202
|
-
props = span[:properties] || {}
|
|
203
|
-
sql_statement = nil
|
|
204
|
-
display_name = if span[:name] == "instantiation.active_record"
|
|
205
|
-
class_name = props["class_name"] || "Record"
|
|
206
|
-
count = props["record_count"]
|
|
207
|
-
count ? "#{class_name} Instantiation (#{count})" : "#{class_name} Instantiation"
|
|
208
|
-
elsif category == "sql"
|
|
209
|
-
sql_statement = props["db.statement"]
|
|
210
|
-
props["name"] || span[:name]
|
|
211
|
-
elsif category == "cache"
|
|
212
|
-
key = props["key"]
|
|
213
|
-
op = span[:name].to_s.sub(".active_support", "").sub("cache_", "")
|
|
214
|
-
key ? "#{op}: #{key.to_s.truncate(80)}" : span[:name]
|
|
215
|
-
elsif category == "view"
|
|
216
|
-
identifier = props["identifier"] || props["code.filepath"]
|
|
217
|
-
if identifier
|
|
218
|
-
identifier.to_s.sub(/^.*\/app\/views\//, "")
|
|
219
|
-
else
|
|
220
|
-
span[:name]
|
|
221
|
-
end
|
|
222
|
-
elsif category == "http"
|
|
223
|
-
url = props["http.url"] || props["http.target"]
|
|
224
|
-
method = props["http.method"]
|
|
225
|
-
"#{method} #{url}".strip.presence || span[:name]
|
|
226
|
-
elsif category == "controller"
|
|
227
|
-
ns = props["code.namespace"]
|
|
228
|
-
fn = props["code.function"]
|
|
229
|
-
ns && fn ? "#{ns}##{fn}" : span[:name]
|
|
230
|
-
else
|
|
231
|
-
span[:name]
|
|
232
|
-
end
|
|
233
|
-
%>
|
|
234
|
-
<div class="waterfall-row" onclick="toggleSpan(<%= index %>)" id="row-<%= index %>" data-span-type="<%= category %>" <% if sql_statement.present? %>data-sql="<%= sql_statement.truncate(500).gsub('"', "'") %>"<% end %>>
|
|
235
|
-
<div class="waterfall-bar-container">
|
|
236
|
-
<div class="waterfall-bar <%= category %>" style="left: <%= left %>%; width: <%= width %>%;"></div>
|
|
237
|
-
<span class="waterfall-label"><%= display_name %></span>
|
|
238
|
-
<span class="waterfall-duration"><%= format_duration(span[:duration_ms]) %></span>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
<div class="span-detail <%= category %>" id="span-<%= index %>">
|
|
242
|
-
<pre><code><%= format_content(span[:properties]) %></code></pre>
|
|
243
|
-
</div>
|
|
244
|
-
<% end %>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
<% end %>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
|
|
252
|
-
<script>
|
|
253
|
-
function showTab(tabName) {
|
|
254
|
-
document.querySelectorAll('.tab-content').forEach(function(el) { el.classList.remove('active'); });
|
|
255
|
-
document.querySelectorAll('.tab').forEach(function(el) { el.classList.remove('active'); });
|
|
256
|
-
document.getElementById('tab-' + tabName).classList.add('active');
|
|
257
|
-
event.target.classList.add('active');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function toggleSpan(index) {
|
|
261
|
-
const detail = document.getElementById('span-' + index);
|
|
262
|
-
const row = document.getElementById('row-' + index);
|
|
263
|
-
if (detail.classList.contains('visible')) {
|
|
264
|
-
detail.classList.remove('visible');
|
|
265
|
-
row.classList.remove('selected');
|
|
266
|
-
} else {
|
|
267
|
-
detail.classList.add('visible');
|
|
268
|
-
row.classList.add('selected');
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function collapseAll() {
|
|
273
|
-
document.querySelectorAll('.span-detail.visible').forEach(function(el) { el.classList.remove('visible'); });
|
|
274
|
-
document.querySelectorAll('.waterfall-row.selected').forEach(function(el) { el.classList.remove('selected'); });
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function filterSpans(filterType) {
|
|
278
|
-
document.querySelectorAll('.legend-item.clickable').forEach(function(el) { el.classList.remove('active'); });
|
|
279
|
-
document.querySelector('.legend-item[data-filter="' + filterType + '"]').classList.add('active');
|
|
280
|
-
document.querySelectorAll('.waterfall-row').forEach(function(row) {
|
|
281
|
-
const spanType = row.getAttribute('data-span-type');
|
|
282
|
-
if (filterType === 'all' || spanType === filterType) {
|
|
283
|
-
row.classList.remove('hidden');
|
|
284
|
-
} else {
|
|
285
|
-
row.classList.add('hidden');
|
|
286
|
-
row.classList.remove('selected');
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
document.querySelectorAll('.span-detail').forEach(function(detail) {
|
|
290
|
-
const index = detail.id.replace('span-', '');
|
|
291
|
-
const row = document.getElementById('row-' + index);
|
|
292
|
-
if (row.classList.contains('hidden')) {
|
|
293
|
-
detail.classList.add('hidden-by-filter');
|
|
294
|
-
detail.classList.remove('visible');
|
|
295
|
-
} else {
|
|
296
|
-
detail.classList.remove('hidden-by-filter');
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// SQL tooltip
|
|
302
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
303
|
-
const sqlTooltip = document.getElementById('sql-tooltip');
|
|
304
|
-
let tooltipTimeout;
|
|
305
|
-
document.querySelectorAll('.waterfall-row[data-sql]').forEach(function(row) {
|
|
306
|
-
row.addEventListener('mouseenter', function() {
|
|
307
|
-
const sql = row.getAttribute('data-sql');
|
|
308
|
-
if (sql && !row.classList.contains('selected')) {
|
|
309
|
-
tooltipTimeout = setTimeout(function() {
|
|
310
|
-
sqlTooltip.textContent = sql;
|
|
311
|
-
sqlTooltip.classList.add('visible');
|
|
312
|
-
}, 200);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
row.addEventListener('mouseleave', function() {
|
|
316
|
-
clearTimeout(tooltipTimeout);
|
|
317
|
-
sqlTooltip.classList.remove('visible');
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
</script>
|
|
322
|
-
|
|
323
|
-
<div id="sql-tooltip"></div>
|