flare 0.1.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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/app/controllers/flare/application_controller.rb +22 -0
- data/app/controllers/flare/jobs_controller.rb +55 -0
- data/app/controllers/flare/requests_controller.rb +73 -0
- data/app/controllers/flare/spans_controller.rb +101 -0
- data/app/helpers/flare/application_helper.rb +168 -0
- data/app/views/flare/jobs/index.html.erb +69 -0
- data/app/views/flare/jobs/show.html.erb +323 -0
- data/app/views/flare/requests/index.html.erb +120 -0
- data/app/views/flare/requests/show.html.erb +498 -0
- data/app/views/flare/spans/index.html.erb +112 -0
- data/app/views/flare/spans/show.html.erb +184 -0
- data/app/views/layouts/flare/application.html.erb +126 -0
- data/config/routes.rb +20 -0
- data/exe/flare +9 -0
- data/lib/flare/backoff_policy.rb +73 -0
- data/lib/flare/cli/doctor_command.rb +129 -0
- data/lib/flare/cli/output.rb +45 -0
- data/lib/flare/cli/setup_command.rb +404 -0
- data/lib/flare/cli/status_command.rb +47 -0
- data/lib/flare/cli.rb +50 -0
- data/lib/flare/configuration.rb +121 -0
- data/lib/flare/engine.rb +43 -0
- data/lib/flare/http_metrics_config.rb +101 -0
- data/lib/flare/metric_counter.rb +45 -0
- data/lib/flare/metric_flusher.rb +124 -0
- data/lib/flare/metric_key.rb +42 -0
- data/lib/flare/metric_span_processor.rb +470 -0
- data/lib/flare/metric_storage.rb +42 -0
- data/lib/flare/metric_submitter.rb +221 -0
- data/lib/flare/source_location.rb +113 -0
- data/lib/flare/sqlite_exporter.rb +279 -0
- data/lib/flare/storage/sqlite.rb +789 -0
- data/lib/flare/storage.rb +54 -0
- data/lib/flare/version.rb +5 -0
- data/lib/flare.rb +411 -0
- data/public/flare-assets/flare.css +1245 -0
- data/public/flare-assets/images/flipper.png +0 -0
- metadata +240 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<%
|
|
2
|
+
props = @span[:properties] || {}
|
|
3
|
+
events = @span[:events] || []
|
|
4
|
+
|
|
5
|
+
started_at = Time.at(@span[:start_timestamp] / 1_000_000_000.0) rescue nil
|
|
6
|
+
|
|
7
|
+
# Determine badge class based on category
|
|
8
|
+
badge_class = case @category
|
|
9
|
+
when "queries" then "sql"
|
|
10
|
+
when "cache" then "cache"
|
|
11
|
+
when "views" then "view"
|
|
12
|
+
when "http" then "http"
|
|
13
|
+
when "mail" then "mail"
|
|
14
|
+
when "exceptions" then "exception"
|
|
15
|
+
else "other"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get display name
|
|
19
|
+
display_name = @span[:name]
|
|
20
|
+
|
|
21
|
+
# Get statement/content for code block
|
|
22
|
+
statement = case @category
|
|
23
|
+
when "queries"
|
|
24
|
+
props["db.statement"]
|
|
25
|
+
when "redis"
|
|
26
|
+
props["db.statement"]
|
|
27
|
+
when "http"
|
|
28
|
+
nil # No statement block for HTTP
|
|
29
|
+
else
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Root span link
|
|
34
|
+
root_path = if @span[:root_trace_id]
|
|
35
|
+
if @span[:root_kind] == "consumer"
|
|
36
|
+
job_path(@span[:root_trace_id])
|
|
37
|
+
else
|
|
38
|
+
request_path(@span[:root_trace_id])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
root_label = if @span[:root_trace_id]
|
|
43
|
+
if @span[:root_kind] == "consumer"
|
|
44
|
+
# For jobs, code.namespace contains the job class name
|
|
45
|
+
@span[:root_controller] || @span[:root_name].to_s.sub(/ (process|publish)$/, '')
|
|
46
|
+
elsif @span[:root_controller] && @span[:root_action]
|
|
47
|
+
# For requests, show controller#action
|
|
48
|
+
"#{@span[:root_controller]}##{@span[:root_action]}"
|
|
49
|
+
else
|
|
50
|
+
@span[:root_name]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
%>
|
|
54
|
+
|
|
55
|
+
<div class="page-header">
|
|
56
|
+
<div class="detail-header">
|
|
57
|
+
<div class="detail-header-left">
|
|
58
|
+
<div class="detail-title">
|
|
59
|
+
<span class="badge badge-<%= badge_class %>"><%= @category&.singularize&.titleize || "Span" %></span>
|
|
60
|
+
<h1><%= display_name %></h1>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="detail-meta">
|
|
63
|
+
<div class="meta-item">
|
|
64
|
+
<span class="meta-label">Duration</span>
|
|
65
|
+
<span class="meta-value mono"><%= format_duration(@span[:duration_ms]) %></span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="meta-item">
|
|
68
|
+
<span class="meta-label">Time</span>
|
|
69
|
+
<span class="meta-value"><%= started_at&.strftime("%b %d, %Y %H:%M:%S") %></span>
|
|
70
|
+
</div>
|
|
71
|
+
<% if root_path %>
|
|
72
|
+
<div class="meta-item">
|
|
73
|
+
<span class="meta-label">Parent</span>
|
|
74
|
+
<span class="meta-value"><a href="<%= root_path %>"><%= root_label %></a></span>
|
|
75
|
+
</div>
|
|
76
|
+
<% end %>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="page-body">
|
|
83
|
+
<div class="card">
|
|
84
|
+
<%
|
|
85
|
+
# Filter properties for display
|
|
86
|
+
display_props = props.reject do |key, value|
|
|
87
|
+
key == "db.statement" || key == "code.stacktrace" || value.is_a?(Hash) || value.is_a?(Array)
|
|
88
|
+
end
|
|
89
|
+
%>
|
|
90
|
+
<div class="details-sections">
|
|
91
|
+
<% if display_props.any? %>
|
|
92
|
+
<!-- Properties Section -->
|
|
93
|
+
<div class="details-section">
|
|
94
|
+
<div class="section-header">
|
|
95
|
+
<div class="section-icon properties">
|
|
96
|
+
<i class="bi bi-file-text"></i>
|
|
97
|
+
</div>
|
|
98
|
+
<span class="section-title">Properties</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="section-grid">
|
|
101
|
+
<% display_props.each do |key, value| %>
|
|
102
|
+
<div class="detail-item">
|
|
103
|
+
<span class="detail-label"><%= key.to_s.split(".").last.titleize %></span>
|
|
104
|
+
<span class="detail-value <%= 'mono' if key.include?('.') %>" title="<%= value %>"><%= value %></span>
|
|
105
|
+
</div>
|
|
106
|
+
<% end %>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<% end %>
|
|
110
|
+
|
|
111
|
+
<% if statement.present? %>
|
|
112
|
+
<!-- Statement Section -->
|
|
113
|
+
<div class="details-section">
|
|
114
|
+
<div class="section-header">
|
|
115
|
+
<div class="section-icon statement">
|
|
116
|
+
<i class="bi bi-code-slash"></i>
|
|
117
|
+
</div>
|
|
118
|
+
<span class="section-title">Statement</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="statement-block">
|
|
121
|
+
<pre><code><%= statement %></code></pre>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<% end %>
|
|
125
|
+
|
|
126
|
+
<% if props["code.stacktrace"].present? %>
|
|
127
|
+
<!-- Trace Section -->
|
|
128
|
+
<div class="details-section">
|
|
129
|
+
<div class="section-header">
|
|
130
|
+
<div class="section-icon trace">
|
|
131
|
+
<i class="bi bi-stack"></i>
|
|
132
|
+
</div>
|
|
133
|
+
<span class="section-title">Source Trace</span>
|
|
134
|
+
</div>
|
|
135
|
+
<ul class="stacktrace-list">
|
|
136
|
+
<% props["code.stacktrace"].split("\n").each do |line| %>
|
|
137
|
+
<li>
|
|
138
|
+
<% if line =~ /\A(.+):(\d+) in `(.+)'\z/ %>
|
|
139
|
+
<span class="file-path"><%= $1 %></span><span class="line-number">:<%= $2 %></span> in <span class="method-name">`<%= $3 %>'</span>
|
|
140
|
+
<% elsif line =~ /\A(.+):(\d+)\z/ %>
|
|
141
|
+
<span class="file-path"><%= $1 %></span><span class="line-number">:<%= $2 %></span>
|
|
142
|
+
<% else %>
|
|
143
|
+
<%= line %>
|
|
144
|
+
<% end %>
|
|
145
|
+
</li>
|
|
146
|
+
<% end %>
|
|
147
|
+
</ul>
|
|
148
|
+
</div>
|
|
149
|
+
<% end %>
|
|
150
|
+
|
|
151
|
+
<% if events.any? %>
|
|
152
|
+
<!-- Events Section -->
|
|
153
|
+
<div class="details-section">
|
|
154
|
+
<div class="section-header">
|
|
155
|
+
<div class="section-icon events">
|
|
156
|
+
<i class="bi bi-lightning"></i>
|
|
157
|
+
</div>
|
|
158
|
+
<span class="section-title">Events (<%= events.size %>)</span>
|
|
159
|
+
</div>
|
|
160
|
+
<% events.each do |event| %>
|
|
161
|
+
<div class="event-item">
|
|
162
|
+
<div class="event-name"><%= event[:name] %></div>
|
|
163
|
+
<% if event[:properties].present? %>
|
|
164
|
+
<div class="event-props">
|
|
165
|
+
<% event[:properties].each do |key, value| %>
|
|
166
|
+
<div><strong><%= key %>:</strong> <%= value.to_s.truncate(500) %></div>
|
|
167
|
+
<% end %>
|
|
168
|
+
</div>
|
|
169
|
+
<% end %>
|
|
170
|
+
</div>
|
|
171
|
+
<% end %>
|
|
172
|
+
</div>
|
|
173
|
+
<% end %>
|
|
174
|
+
|
|
175
|
+
<% if display_props.blank? && statement.blank? && props["code.stacktrace"].blank? && events.empty? %>
|
|
176
|
+
<div class="details-section">
|
|
177
|
+
<div class="empty-clues" style="padding: 1.25rem;">
|
|
178
|
+
<p>No additional details available for this span.</p>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<% end %>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Flare</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
|
8
|
+
<link href="/flare-assets/flare.css" rel="stylesheet">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="app-layout">
|
|
12
|
+
<aside class="sidebar">
|
|
13
|
+
<div class="sidebar-header">
|
|
14
|
+
<a href="<%= flare.root_path %>" class="logo">
|
|
15
|
+
<div class="logo-icon">
|
|
16
|
+
<i class="bi bi-train-front"></i>
|
|
17
|
+
</div>
|
|
18
|
+
<span class="logo-text">Flare</span>
|
|
19
|
+
</a>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<nav class="sidebar-nav">
|
|
23
|
+
<div class="nav-section">
|
|
24
|
+
<div class="nav-section-title">Traces</div>
|
|
25
|
+
|
|
26
|
+
<a href="<%= flare.requests_path %>" class="nav-item <%= 'active' if current_section == 'requests' %>">
|
|
27
|
+
<i class="bi bi-arrow-left-right"></i>
|
|
28
|
+
<span>Requests</span>
|
|
29
|
+
</a>
|
|
30
|
+
|
|
31
|
+
<a href="<%= flare.jobs_path %>" class="nav-item <%= 'active' if current_section == 'jobs' %>">
|
|
32
|
+
<i class="bi bi-briefcase"></i>
|
|
33
|
+
<span>Jobs</span>
|
|
34
|
+
</a>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="nav-section">
|
|
38
|
+
<div class="nav-section-title">Spans</div>
|
|
39
|
+
|
|
40
|
+
<a href="<%= flare.exceptions_spans_path %>" class="nav-item <%= 'active' if current_section == 'exceptions' %>">
|
|
41
|
+
<i class="bi bi-exclamation-triangle"></i>
|
|
42
|
+
<span>Exceptions</span>
|
|
43
|
+
</a>
|
|
44
|
+
|
|
45
|
+
<a href="<%= flare.queries_spans_path %>" class="nav-item <%= 'active' if current_section == 'queries' %>">
|
|
46
|
+
<i class="bi bi-database"></i>
|
|
47
|
+
<span>Queries</span>
|
|
48
|
+
</a>
|
|
49
|
+
|
|
50
|
+
<a href="<%= flare.cache_spans_path %>" class="nav-item <%= 'active' if current_section == 'cache' %>">
|
|
51
|
+
<i class="bi bi-archive"></i>
|
|
52
|
+
<span>Cache</span>
|
|
53
|
+
</a>
|
|
54
|
+
|
|
55
|
+
<a href="<%= flare.views_spans_path %>" class="nav-item <%= 'active' if current_section == 'views' %>">
|
|
56
|
+
<i class="bi bi-layout-sidebar-inset"></i>
|
|
57
|
+
<span>Views</span>
|
|
58
|
+
</a>
|
|
59
|
+
|
|
60
|
+
<a href="<%= flare.http_spans_path %>" class="nav-item <%= 'active' if current_section == 'http' %>">
|
|
61
|
+
<i class="bi bi-globe"></i>
|
|
62
|
+
<span>HTTP</span>
|
|
63
|
+
</a>
|
|
64
|
+
|
|
65
|
+
<a href="<%= flare.mail_spans_path %>" class="nav-item <%= 'active' if current_section == 'mail' %>">
|
|
66
|
+
<i class="bi bi-envelope"></i>
|
|
67
|
+
<span>Mail</span>
|
|
68
|
+
</a>
|
|
69
|
+
|
|
70
|
+
<% if show_redis_tab? %>
|
|
71
|
+
<a href="<%= flare.redis_spans_path %>" class="nav-item <%= 'active' if current_section == 'redis' %>">
|
|
72
|
+
<i class="bi bi-layers"></i>
|
|
73
|
+
<span>Redis</span>
|
|
74
|
+
</a>
|
|
75
|
+
<% end %>
|
|
76
|
+
</div>
|
|
77
|
+
</nav>
|
|
78
|
+
|
|
79
|
+
<div class="sidebar-footer">
|
|
80
|
+
<a href="https://www.flippercloud.io?utm_source=flare&utm_medium=dashboard&utm_campaign=sidebar_ad" target="_blank" rel="noopener" class="sidebar-ad">
|
|
81
|
+
<div class="sidebar-ad-inner">
|
|
82
|
+
<div class="sidebar-ad-logo">
|
|
83
|
+
<img src="/flare-assets/images/flipper.png" alt="Flipper">
|
|
84
|
+
</div>
|
|
85
|
+
<div class="sidebar-ad-text">
|
|
86
|
+
<div class="sidebar-ad-title">Flipper</div>
|
|
87
|
+
Ship features safely.<br>Roll back instantly.
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</a>
|
|
91
|
+
<%= button_to flare.clear_data_path, method: :delete, class: "btn-clear", data: { confirm: "Clear all data? This cannot be undone." } do %>
|
|
92
|
+
<i class="bi bi-trash3"></i>
|
|
93
|
+
<span>Clear Data</span>
|
|
94
|
+
<% end %>
|
|
95
|
+
</div>
|
|
96
|
+
</aside>
|
|
97
|
+
|
|
98
|
+
<main class="main-content">
|
|
99
|
+
<% if flash[:alert] %>
|
|
100
|
+
<div style="padding: 1.25rem 1.5rem 0;">
|
|
101
|
+
<div class="alert alert-danger"><%= flash[:alert] %></div>
|
|
102
|
+
</div>
|
|
103
|
+
<% end %>
|
|
104
|
+
<%= yield %>
|
|
105
|
+
</main>
|
|
106
|
+
</div>
|
|
107
|
+
<script>
|
|
108
|
+
// Handle data-confirm on buttons/forms
|
|
109
|
+
document.addEventListener('click', function(e) {
|
|
110
|
+
const button = e.target.closest('[data-confirm]');
|
|
111
|
+
if (button && !confirm(button.dataset.confirm)) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Navigate to URL only if user isn't selecting text
|
|
117
|
+
function navigateIfNotSelecting(url) {
|
|
118
|
+
const selection = window.getSelection();
|
|
119
|
+
if (selection && selection.toString().length > 0) {
|
|
120
|
+
return; // User is selecting text, don't navigate
|
|
121
|
+
}
|
|
122
|
+
window.location = url;
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Flare::Engine.routes.draw do
|
|
4
|
+
resources :requests, only: [:index, :show]
|
|
5
|
+
resources :jobs, only: [:index, :show]
|
|
6
|
+
|
|
7
|
+
# Span category routes
|
|
8
|
+
get "spans/queries", to: "spans#queries", as: :queries_spans
|
|
9
|
+
get "spans/cache", to: "spans#cache", as: :cache_spans
|
|
10
|
+
get "spans/views", to: "spans#views", as: :views_spans
|
|
11
|
+
get "spans/http", to: "spans#http", as: :http_spans
|
|
12
|
+
get "spans/mail", to: "spans#mail", as: :mail_spans
|
|
13
|
+
get "spans/redis", to: "spans#redis", as: :redis_spans
|
|
14
|
+
get "spans/exceptions", to: "spans#exceptions", as: :exceptions_spans
|
|
15
|
+
get "spans/:id", to: "spans#show", as: :span
|
|
16
|
+
|
|
17
|
+
delete "clear", to: "requests#clear", as: :clear_data
|
|
18
|
+
|
|
19
|
+
root to: "requests#index"
|
|
20
|
+
end
|
data/exe/flare
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flare
|
|
4
|
+
# Exponential backoff with jitter for retry logic.
|
|
5
|
+
# Based on Flipper's implementation.
|
|
6
|
+
class BackoffPolicy
|
|
7
|
+
# Default minimum timeout between intervals in milliseconds
|
|
8
|
+
MIN_TIMEOUT_MS = 1_000 # 1 second
|
|
9
|
+
|
|
10
|
+
# Default maximum timeout between intervals in milliseconds
|
|
11
|
+
MAX_TIMEOUT_MS = 30_000 # 30 seconds
|
|
12
|
+
|
|
13
|
+
# Value to multiply the current interval with for each retry attempt
|
|
14
|
+
MULTIPLIER = 1.5
|
|
15
|
+
|
|
16
|
+
# Randomization factor to create a range around the retry interval
|
|
17
|
+
RANDOMIZATION_FACTOR = 0.5
|
|
18
|
+
|
|
19
|
+
attr_reader :min_timeout_ms, :max_timeout_ms, :multiplier, :randomization_factor
|
|
20
|
+
attr_reader :attempts
|
|
21
|
+
|
|
22
|
+
def initialize(options = {})
|
|
23
|
+
@min_timeout_ms = options.fetch(:min_timeout_ms) {
|
|
24
|
+
ENV.fetch("FLARE_BACKOFF_MIN_TIMEOUT_MS", MIN_TIMEOUT_MS).to_i
|
|
25
|
+
}
|
|
26
|
+
@max_timeout_ms = options.fetch(:max_timeout_ms) {
|
|
27
|
+
ENV.fetch("FLARE_BACKOFF_MAX_TIMEOUT_MS", MAX_TIMEOUT_MS).to_i
|
|
28
|
+
}
|
|
29
|
+
@multiplier = options.fetch(:multiplier) {
|
|
30
|
+
ENV.fetch("FLARE_BACKOFF_MULTIPLIER", MULTIPLIER).to_f
|
|
31
|
+
}
|
|
32
|
+
@randomization_factor = options.fetch(:randomization_factor) {
|
|
33
|
+
ENV.fetch("FLARE_BACKOFF_RANDOMIZATION_FACTOR", RANDOMIZATION_FACTOR).to_f
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
validate!
|
|
37
|
+
@attempts = 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the next backoff interval in milliseconds.
|
|
41
|
+
def next_interval
|
|
42
|
+
interval = @min_timeout_ms * (@multiplier**@attempts)
|
|
43
|
+
interval = add_jitter(interval, @randomization_factor)
|
|
44
|
+
|
|
45
|
+
@attempts += 1
|
|
46
|
+
|
|
47
|
+
# Cap the interval to the max timeout
|
|
48
|
+
result = [interval, @max_timeout_ms].min
|
|
49
|
+
# Add small jitter even when maxed out
|
|
50
|
+
result == @max_timeout_ms ? add_jitter(result, 0.05) : result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reset
|
|
54
|
+
@attempts = 0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def validate!
|
|
60
|
+
raise ArgumentError, ":min_timeout_ms must be >= 0" unless @min_timeout_ms >= 0
|
|
61
|
+
raise ArgumentError, ":max_timeout_ms must be >= 0" unless @max_timeout_ms >= 0
|
|
62
|
+
raise ArgumentError, ":min_timeout_ms must be <= :max_timeout_ms" unless @min_timeout_ms <= @max_timeout_ms
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def add_jitter(base, randomization_factor)
|
|
66
|
+
random_number = rand
|
|
67
|
+
max_deviation = base * randomization_factor
|
|
68
|
+
deviation = random_number * max_deviation
|
|
69
|
+
|
|
70
|
+
random_number < 0.5 ? base - deviation : base + deviation
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "output"
|
|
4
|
+
|
|
5
|
+
module Flare
|
|
6
|
+
class DoctorCommand
|
|
7
|
+
include CLI::Output
|
|
8
|
+
|
|
9
|
+
def run
|
|
10
|
+
puts bold("Flare Doctor")
|
|
11
|
+
puts
|
|
12
|
+
|
|
13
|
+
results = []
|
|
14
|
+
results << check_initializer
|
|
15
|
+
results << check_key
|
|
16
|
+
results << check_gitignore
|
|
17
|
+
results << check_database if spans_expected?
|
|
18
|
+
|
|
19
|
+
puts
|
|
20
|
+
if results.all?
|
|
21
|
+
puts green("Everything looks good!")
|
|
22
|
+
else
|
|
23
|
+
puts "Run #{bold("flare setup")} to fix issues."
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def check_initializer
|
|
30
|
+
path = File.join(Dir.pwd, "config/initializers/flare.rb")
|
|
31
|
+
if File.exist?(path)
|
|
32
|
+
puts " #{checkmark} Initializer exists"
|
|
33
|
+
true
|
|
34
|
+
else
|
|
35
|
+
puts " #{xmark} Initializer not found"
|
|
36
|
+
puts " Run #{bold("flare setup")} to create one"
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def check_key
|
|
42
|
+
if key_configured?
|
|
43
|
+
puts " #{checkmark} FLARE_KEY configured"
|
|
44
|
+
true
|
|
45
|
+
elsif credentials_exist?
|
|
46
|
+
puts " #{checkmark} FLARE_KEY not in ENV or .env (may be in Rails credentials)"
|
|
47
|
+
true
|
|
48
|
+
else
|
|
49
|
+
puts " #{xmark} FLARE_KEY not found"
|
|
50
|
+
puts " Run #{bold("flare setup")} to authenticate"
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_gitignore
|
|
56
|
+
gitignore_path = File.join(Dir.pwd, ".gitignore")
|
|
57
|
+
|
|
58
|
+
unless File.exist?(gitignore_path)
|
|
59
|
+
puts " #{warn_mark} No .gitignore file found"
|
|
60
|
+
return true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
contents = File.read(gitignore_path)
|
|
64
|
+
missing = []
|
|
65
|
+
missing << ".env" unless contents.match?(/^\.env$/)
|
|
66
|
+
missing << "flare.sqlite3*" unless contents.include?("flare.sqlite3")
|
|
67
|
+
|
|
68
|
+
if missing.empty?
|
|
69
|
+
puts " #{checkmark} .gitignore entries present"
|
|
70
|
+
true
|
|
71
|
+
else
|
|
72
|
+
puts " #{warn_mark} .gitignore missing: #{missing.join(", ")}"
|
|
73
|
+
puts " Run #{bold("flare setup")} to add them"
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def check_database
|
|
79
|
+
db_path = File.join(Dir.pwd, "db", "flare.sqlite3")
|
|
80
|
+
db_dir = File.dirname(db_path)
|
|
81
|
+
|
|
82
|
+
if File.exist?(db_path)
|
|
83
|
+
if File.writable?(db_path)
|
|
84
|
+
puts " #{checkmark} Database exists and is writable"
|
|
85
|
+
true
|
|
86
|
+
else
|
|
87
|
+
puts " #{xmark} Database exists but is not writable"
|
|
88
|
+
puts " Check file permissions on #{db_path}"
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
elsif File.exist?(db_dir) && File.writable?(db_dir)
|
|
92
|
+
puts " #{checkmark} Database directory is writable (will be created on first request)"
|
|
93
|
+
true
|
|
94
|
+
else
|
|
95
|
+
puts " #{warn_mark} Database directory #{db_dir} does not exist"
|
|
96
|
+
puts " It will be created when you start your Rails server"
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def key_configured?
|
|
102
|
+
return true if ENV["FLARE_KEY"] && !ENV["FLARE_KEY"].empty?
|
|
103
|
+
|
|
104
|
+
env_path = File.join(Dir.pwd, ".env")
|
|
105
|
+
File.exist?(env_path) && File.read(env_path).match?(/^FLARE_KEY=.+/)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def credentials_exist?
|
|
109
|
+
%w[config/credentials.yml.enc config/credentials/production.yml.enc].any? do |path|
|
|
110
|
+
File.exist?(File.join(Dir.pwd, path))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def spans_expected?
|
|
115
|
+
# In production, spans are off by default — skip database check
|
|
116
|
+
env = ENV.fetch("RAILS_ENV", "development")
|
|
117
|
+
return false if env == "production"
|
|
118
|
+
|
|
119
|
+
# If explicitly disabled in the initializer, skip database check
|
|
120
|
+
init_path = File.join(Dir.pwd, "config/initializers/flare.rb")
|
|
121
|
+
if File.exist?(init_path)
|
|
122
|
+
content = File.read(init_path)
|
|
123
|
+
return false if content.match?(/^\s*config\.spans_enabled\s*=\s*false/)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flare
|
|
4
|
+
module CLI
|
|
5
|
+
module Output
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def color?
|
|
9
|
+
$stdout.tty?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def green(text)
|
|
13
|
+
color? ? "\e[32m#{text}\e[0m" : text
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def yellow(text)
|
|
17
|
+
color? ? "\e[33m#{text}\e[0m" : text
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def red(text)
|
|
21
|
+
color? ? "\e[31m#{text}\e[0m" : text
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def bold(text)
|
|
25
|
+
color? ? "\e[1m#{text}\e[0m" : text
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def dim(text)
|
|
29
|
+
color? ? "\e[2m#{text}\e[0m" : text
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def checkmark
|
|
33
|
+
green("✓")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def xmark
|
|
37
|
+
red("✗")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def warn_mark
|
|
41
|
+
yellow("!")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|