rails_observatory 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +42 -0
- data/Rakefile +8 -0
- data/app/assets/config/rails_observatory_manifest.js +2 -0
- data/app/assets/images/rails_observatory/logo.svg +8 -0
- data/app/assets/js/application.js +88 -0
- data/app/assets/js/controllers/chart_controller.js +176 -0
- data/app/assets/js/controllers/event_details_controller.js +15 -0
- data/app/assets/js/controllers/index.js +9 -0
- data/app/assets/js/controllers/sparkline_controller.js +72 -0
- data/app/assets/stylesheets/application/card.css +51 -0
- data/app/assets/stylesheets/application/chart.css +34 -0
- data/app/assets/stylesheets/application/dropdown.css +62 -0
- data/app/assets/stylesheets/application/global_modifiers.css +10 -0
- data/app/assets/stylesheets/application/query_table.css +68 -0
- data/app/assets/stylesheets/application/side_nav.css +62 -0
- data/app/assets/stylesheets/application/side_panel.css +35 -0
- data/app/assets/stylesheets/application/tab_nav.css +64 -0
- data/app/assets/stylesheets/application/table_chart.css +66 -0
- data/app/assets/stylesheets/application/tbd.css +70 -0
- data/app/assets/stylesheets/application/top_nav.css +33 -0
- data/app/assets/stylesheets/application.css +42 -0
- data/app/assets/stylesheets/elements/a.css +8 -0
- data/app/assets/stylesheets/elements/button.css +21 -0
- data/app/assets/stylesheets/elements/details.css +12 -0
- data/app/assets/stylesheets/elements/root.css +26 -0
- data/app/assets/stylesheets/elements/section.css +9 -0
- data/app/assets/stylesheets/errors/show/details.css +13 -0
- data/app/assets/stylesheets/layout/app.css +23 -0
- data/app/assets/stylesheets/layout/details-side-panel.css +15 -0
- data/app/assets/stylesheets/layout/requests.css +45 -0
- data/app/assets/stylesheets/layout/two-column.css +17 -0
- data/app/assets/stylesheets/mixins/nav_button.css +19 -0
- data/app/assets/stylesheets/requests/stats.css +35 -0
- data/app/controllers/rails_observatory/application_controller.rb +24 -0
- data/app/controllers/rails_observatory/errors_controller.rb +27 -0
- data/app/controllers/rails_observatory/jobs_controller.rb +25 -0
- data/app/controllers/rails_observatory/mailers_controller.rb +11 -0
- data/app/controllers/rails_observatory/requests_controller.rb +33 -0
- data/app/helpers/rails_observatory/application_helper.rb +110 -0
- data/app/jobs/rails_observatory/application_job.rb +4 -0
- data/app/mailers/rails_observatory/application_mailer.rb +6 -0
- data/app/views/layouts/rails_observatory/application.html.erb +93 -0
- data/app/views/new_user_mailer/greeting.html.erb +1 -0
- data/app/views/posts/index.html.erb +1 -0
- data/app/views/rails_observatory/application/_chart.html.erb +23 -0
- data/app/views/rails_observatory/application/_events_table.html.erb +24 -0
- data/app/views/rails_observatory/application/_sparkline.html.erb +17 -0
- data/app/views/rails_observatory/application/_trace.html.erb +122 -0
- data/app/views/rails_observatory/errors/index.html.erb +87 -0
- data/app/views/rails_observatory/errors/show.html.erb +193 -0
- data/app/views/rails_observatory/jobs/_table_chart.html.erb +29 -0
- data/app/views/rails_observatory/jobs/index.html.erb +20 -0
- data/app/views/rails_observatory/jobs/show.html.erb +8 -0
- data/app/views/rails_observatory/logs/index.html.erb +18 -0
- data/app/views/rails_observatory/mailers/index.html.erb +11 -0
- data/app/views/rails_observatory/mailers/show.html.erb +10 -0
- data/app/views/rails_observatory/requests/_text_gauge.html.erb +4 -0
- data/app/views/rails_observatory/requests/index.html.erb +56 -0
- data/app/views/rails_observatory/requests/show.html.erb +16 -0
- data/config/routes.rb +7 -0
- data/lib/rails_observatory/action_mailer_subscriber.rb +14 -0
- data/lib/rails_observatory/engine.rb +49 -0
- data/lib/rails_observatory/event_collector.rb +43 -0
- data/lib/rails_observatory/log_collector.rb +46 -0
- data/lib/rails_observatory/mailer_previews/delivered_mail_preview.rb +9 -0
- data/lib/rails_observatory/middleware.rb +77 -0
- data/lib/rails_observatory/models/error.rb +67 -0
- data/lib/rails_observatory/models/event_collection.rb +137 -0
- data/lib/rails_observatory/models/events.rb +22 -0
- data/lib/rails_observatory/models/job_trace.rb +28 -0
- data/lib/rails_observatory/models/logs.rb +9 -0
- data/lib/rails_observatory/models/mail_delivery.rb +33 -0
- data/lib/rails_observatory/models/redis_model.rb +112 -0
- data/lib/rails_observatory/models/request_trace.rb +29 -0
- data/lib/rails_observatory/railties/active_job_instrumentation.rb +48 -0
- data/lib/rails_observatory/railties/redis_runtime.rb +11 -0
- data/lib/rails_observatory/redis/logging_middleware.rb +22 -0
- data/lib/rails_observatory/redis/redis_client_instrumentation.rb +18 -0
- data/lib/rails_observatory/redis/time_series/increment_script.lua +67 -0
- data/lib/rails_observatory/redis/time_series/insertion.rb +73 -0
- data/lib/rails_observatory/redis/time_series/query_builder.rb +149 -0
- data/lib/rails_observatory/redis/time_series/timing_script.lua +89 -0
- data/lib/rails_observatory/redis/time_series.rb +91 -0
- data/lib/rails_observatory/serializers/event_serializer.rb +19 -0
- data/lib/rails_observatory/serializers/headers_serializer.rb +12 -0
- data/lib/rails_observatory/serializers/job_serializer.rb +11 -0
- data/lib/rails_observatory/serializers/mail_delivery_job_serializer.rb +14 -0
- data/lib/rails_observatory/serializers/request_serializer.rb +17 -0
- data/lib/rails_observatory/serializers/response_serializer.rb +14 -0
- data/lib/rails_observatory/serializers/serializer.rb +51 -0
- data/lib/rails_observatory/version.rb +3 -0
- data/lib/rails_observatory.rb +3 -0
- data/public/assets/js/application.js +11186 -0
- data/public/assets/logo_with_text.svg +21 -0
- data/public/assets/stylesheets/application.css +757 -0
- metadata +197 -0
@@ -0,0 +1,193 @@
|
|
1
|
+
<% content_for(:title) do %>
|
2
|
+
<%= @error.class_name %>
|
3
|
+
<span class="_location">
|
4
|
+
in <%= @error.location.classify %>
|
5
|
+
</span>
|
6
|
+
<% end %>
|
7
|
+
<% content_for(:hide_duration, 'true') %>
|
8
|
+
<% content_for(:main_css_class, 'layout-two-column') %>
|
9
|
+
|
10
|
+
<style>
|
11
|
+
|
12
|
+
|
13
|
+
._location {
|
14
|
+
font-weight: 200;
|
15
|
+
}
|
16
|
+
|
17
|
+
|
18
|
+
.hll {
|
19
|
+
background-color: #49483e;
|
20
|
+
/*text-underline-style: wave;*/
|
21
|
+
/*text-decoration: underline;*/
|
22
|
+
|
23
|
+
& pre > span {
|
24
|
+
text-decoration-line: underline;
|
25
|
+
text-decoration-style: wavy;
|
26
|
+
text-decoration-color: var(--red);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
.highlight {
|
31
|
+
/*background-color: var(--surface-card);*/
|
32
|
+
width: 100%;
|
33
|
+
overflow: auto;
|
34
|
+
|
35
|
+
& table {
|
36
|
+
border-spacing: 0;
|
37
|
+
width: 100%;
|
38
|
+
}
|
39
|
+
|
40
|
+
& tr td:first-child {
|
41
|
+
padding-inline: .25rem;
|
42
|
+
position: sticky;
|
43
|
+
left: 0;
|
44
|
+
background-color: #161b22;
|
45
|
+
|
46
|
+
.hll & {
|
47
|
+
background-color: #49483e;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
.backtrace-line {
|
53
|
+
padding: .5rem 1rem;
|
54
|
+
display: flex;
|
55
|
+
flex-direction: column;
|
56
|
+
border-top: 1px solid var(--divider);
|
57
|
+
gap: 1rem;
|
58
|
+
color: var(--black-secondary);
|
59
|
+
|
60
|
+
&.--application-frame {
|
61
|
+
& summary::after {
|
62
|
+
content: 'Application Frame';
|
63
|
+
font-size: .75rem;
|
64
|
+
text-align: right;
|
65
|
+
color: color-mix(in oklab, #0c8be8 5%, var(--white));
|
66
|
+
padding: .1rem .35rem;
|
67
|
+
border-radius: .35rem;
|
68
|
+
font-weight: 400;
|
69
|
+
/* text gradient fill, blue to white */
|
70
|
+
background: color-mix(in oklab, #0c8be8 80%, var(--black));
|
71
|
+
border: 1px solid color-mix(in oklab, #0c8be8 60%, var(--white));
|
72
|
+
/*background-clip: text;*/
|
73
|
+
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
& > :not(summary) {
|
78
|
+
margin-top: 1rem;
|
79
|
+
}
|
80
|
+
|
81
|
+
summary {
|
82
|
+
display: flex;
|
83
|
+
align-items: center;
|
84
|
+
gap: .5rem;
|
85
|
+
color: var(--white);
|
86
|
+
|
87
|
+
& > :not(svg) {
|
88
|
+
flex-grow: 1;
|
89
|
+
}
|
90
|
+
|
91
|
+
& > svg {
|
92
|
+
flex-shrink: 0;
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
._path {
|
97
|
+
white-space: nowrap;
|
98
|
+
overflow: hidden;
|
99
|
+
text-overflow: ellipsis;
|
100
|
+
}
|
101
|
+
|
102
|
+
|
103
|
+
}
|
104
|
+
|
105
|
+
.framework-trace-chunk {
|
106
|
+
border-top: 1px solid var(--divider);
|
107
|
+
font-style: italic;
|
108
|
+
color: var(--black-secondary);
|
109
|
+
|
110
|
+
|
111
|
+
&[open] {
|
112
|
+
|
113
|
+
color: color-mix(in oklab, var(--black-secondary) 40%, var(--white));
|
114
|
+
}
|
115
|
+
|
116
|
+
& > summary {
|
117
|
+
&:hover {
|
118
|
+
/*background-color: var(--surface-active);*/
|
119
|
+
}
|
120
|
+
|
121
|
+
padding: .5rem 1rem;
|
122
|
+
display: flex;
|
123
|
+
gap: .5rem;
|
124
|
+
}
|
125
|
+
}
|
126
|
+
</style>
|
127
|
+
|
128
|
+
<div class="layout-two-column-side-panel side-panel">
|
129
|
+
<dl>
|
130
|
+
<dt>Occurrences</dt>
|
131
|
+
<dd class="_occurrence_count"><%= @count %></dd>
|
132
|
+
<dt>Last Seen</dt>
|
133
|
+
<dd title="<%= @error.time %>"><%= time_ago_in_words(@error.time) %> ago</dd>
|
134
|
+
|
135
|
+
<dt>Past 24 Hours</dt>
|
136
|
+
<dd>
|
137
|
+
<%= render "sparkline", type: "bar", series: @past_24_hours %>
|
138
|
+
</dd>
|
139
|
+
|
140
|
+
<dt>Past 7 Days</dt>
|
141
|
+
<dd>
|
142
|
+
<%= render "sparkline", type: "bar", series: @past_7_days %>
|
143
|
+
</dd>
|
144
|
+
|
145
|
+
<dt>Past 30 Days</dt>
|
146
|
+
<dd><%= render "sparkline", type: "bar", series: @past_30_days %></dd>
|
147
|
+
</dl>
|
148
|
+
</div>
|
149
|
+
|
150
|
+
<div class="layout-two-column-main">
|
151
|
+
<div style="padding: 1rem;padding-top:0;padding-bottom:2rem;">
|
152
|
+
<h2><%= @error.message %></h2>
|
153
|
+
<% @error.causes.each do |cause| %>
|
154
|
+
<div style="color: var(--black-secondary);text-transform: uppercase; font-weight: var(--font-weight-bold);">caused by:</div>
|
155
|
+
<h2><%= cause["class_name"] %></h2>
|
156
|
+
<%= cause['message'] %>
|
157
|
+
<% end %>
|
158
|
+
</div>
|
159
|
+
|
160
|
+
<div class="card">
|
161
|
+
<h2>Trace</h2>
|
162
|
+
<% @error.trace.slice_when { _1["is_application_trace"] != _2["is_application_trace"] }.each do |trace_chunk| %>
|
163
|
+
<% if !trace_chunk.first["is_application_trace"] %>
|
164
|
+
<details class="framework-trace-chunk">
|
165
|
+
<summary>
|
166
|
+
<svg class="arrow-right" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="16">
|
167
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
168
|
+
</svg>
|
169
|
+
Plus <%= trace_chunk.length %> Framework frames
|
170
|
+
</summary>
|
171
|
+
<% end %>
|
172
|
+
<% trace_chunk.each do |trace_line| %>
|
173
|
+
<details class="backtrace-line <%= '--application-frame' if trace_line["is_application_trace"] %>" <%= 'open' if trace_line["is_application_trace"] %>>
|
174
|
+
<summary>
|
175
|
+
<svg class="arrow-right" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="16">
|
176
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
177
|
+
</svg>
|
178
|
+
<span class="_path"><%= trace_line["trace"] %></span>
|
179
|
+
</summary>
|
180
|
+
<div class="highlight">
|
181
|
+
<%#= trace_line %>
|
182
|
+
<%== highlight_source_extract(@error.source_extracts[trace_line["id"]]) %>
|
183
|
+
</div>
|
184
|
+
</details>
|
185
|
+
<% end %>
|
186
|
+
<% if !trace_chunk.first["is_application_trace"] %>
|
187
|
+
</details>
|
188
|
+
<% end %>
|
189
|
+
<% end %>
|
190
|
+
</div>
|
191
|
+
|
192
|
+
|
193
|
+
</div>
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<% hashes = metrics.map { |name, agg| RailsObservatory::TimeSeries.where(name:).group(group_by).send(agg) } %>
|
2
|
+
|
3
|
+
<section>
|
4
|
+
<h2><%= name %></h2>
|
5
|
+
|
6
|
+
<div class="table-chart">
|
7
|
+
<table>
|
8
|
+
<thead>
|
9
|
+
<tr>
|
10
|
+
<th><%= group_by %></th>
|
11
|
+
<% metrics.keys.each do |k| %>
|
12
|
+
<th><%= k %></th>
|
13
|
+
<% end %>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
<tbody>
|
17
|
+
<% hashes.first.each do |label, val| %>
|
18
|
+
<tr>
|
19
|
+
<td><%= label %></td>
|
20
|
+
<td><%= val %></td>
|
21
|
+
<% hashes.slice(1..).each do |h| %>
|
22
|
+
<td><%= h[label] %></td>
|
23
|
+
<% end %>
|
24
|
+
</tr>
|
25
|
+
<% end %>
|
26
|
+
</tbody>
|
27
|
+
</table>
|
28
|
+
</div>
|
29
|
+
</section>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<% content_for(:title, 'Jobs') %>
|
2
|
+
|
3
|
+
<%#= render 'chart', name: 'Latency', series: @latency_series, type: 'area', aggregate_using: :avg %>
|
4
|
+
<div style="padding: 2rem;">
|
5
|
+
<%= render 'chart', name: 'Performed', series: series_for(name: 'job.count', aggregate_using: :sum), type: 'bar' %>
|
6
|
+
</div>
|
7
|
+
|
8
|
+
<%= render 'table_chart', name: 'By Queue', group_by: :queue_name, metrics: { "job.count": :sum, "job.queue_latency": :last } %>
|
9
|
+
<%= render 'table_chart', name: 'By Job', group_by: :job_class, metrics: { "job.count": :sum, latency: :avg, "job.error_count": :sum } %>
|
10
|
+
|
11
|
+
<section>
|
12
|
+
<h2>Recent Jobs</h2>
|
13
|
+
<%= render 'events_table',
|
14
|
+
events: @recent_jobs,
|
15
|
+
fields: [:time, :job_id, :job_class, :duration],
|
16
|
+
formatters: {
|
17
|
+
time: ->(time, event) { link_to Time.at(time).to_fs('%H:%M:%S'), job_path(event.job_id) },
|
18
|
+
}
|
19
|
+
%>
|
20
|
+
</section>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<% content_for :title do %>
|
2
|
+
<%= @job.job_class %><br/>
|
3
|
+
<span style="font-size: 1rem; font-weight: 200;"><%= time_ago_in_words(Time.at(@job.time)) %> ago
|
4
|
+
| <%= @job.duration.round(2) %>ms</span>
|
5
|
+
<% end %>
|
6
|
+
<% content_for(:hide_duration, 'true') %>
|
7
|
+
|
8
|
+
<%= render 'trace', model: @job, events: @events %>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<% content_for(:title, 'Logs') %>
|
2
|
+
|
3
|
+
<style>
|
4
|
+
@scope {
|
5
|
+
:scope {
|
6
|
+
display: grid;
|
7
|
+
gap: 2rem;
|
8
|
+
grid-template-columns: minmax(400px, 1fr);
|
9
|
+
}
|
10
|
+
}
|
11
|
+
</style>
|
12
|
+
|
13
|
+
<%= render 'chart', type: 'bar', series: @logs_by_type, name: 'Log Volume' %>
|
14
|
+
|
15
|
+
<div class="card">
|
16
|
+
<h2>Recent Logs</h2>
|
17
|
+
<%= render 'events_table', events: @recent_logs, fields: [:timestamp, :level, :message, :request_id] %>
|
18
|
+
</div>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<% content_for(:title, 'Mailers') %>
|
2
|
+
<section>
|
3
|
+
<h2>Email Delivered</h2>
|
4
|
+
|
5
|
+
<%= render 'events_table', events: @deliveries, fields: [
|
6
|
+
:time, :to, :from, :subject, :view], formatters: {
|
7
|
+
time: ->(time, event) { Time.at(time).to_fs('%H:%M:%S') },
|
8
|
+
view: ->(view, event) { link_to 'View', preview_mail_path(event.message_id) }
|
9
|
+
}
|
10
|
+
%>
|
11
|
+
</section>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<%= content_for :title, 'Mail' %>
|
2
|
+
<% content_for(:hide_duration, 'true') %>
|
3
|
+
|
4
|
+
<div class="card">
|
5
|
+
<h2>Mail Preview</h2>
|
6
|
+
<div>To: <%= @mail.to.join(', ') %></div>
|
7
|
+
<div>From: <%= @mail.from.join(', ') %></div>
|
8
|
+
<div>Subject: <%= @mail.subject %></div>
|
9
|
+
<%== @mail.body %>
|
10
|
+
</div>
|
@@ -0,0 +1,56 @@
|
|
1
|
+
<% content_for(:title, params[:controller_action].presence || 'Requests') %>
|
2
|
+
<% content_for(:subtitle, 'Requests') if params[:controller_action] %>
|
3
|
+
<% content_for(:main_css_class, 'layout-requests_index') %>
|
4
|
+
<div class="layout-requests_index-glance" style="display:flex;justify-content: space-evenly">
|
5
|
+
<% request_count = series_value(name: "request.count", aggregate_using: :sum) %>
|
6
|
+
<% if request_count.to_i > 0 %>
|
7
|
+
<%= render 'text_gauge', title: 'Requests', value: series_value(name: "request.count", aggregate_using: :sum) %>
|
8
|
+
<%= render 'text_gauge', title: 'Throughput', value: series_value(name: "request.count", aggregate_using: :sum).fdiv(duration / 60).round(2), unit: 'rpm' %>
|
9
|
+
<% end %>
|
10
|
+
</div>
|
11
|
+
<div class="layout-requests_index-chart">
|
12
|
+
<%= render 'chart', name: 'Requests', type: 'bar', series: series_for(name: 'request.count', aggregate_using: :sum, action: params[:controller_action]) %>
|
13
|
+
<%= render 'chart', name: 'Latency', type: 'area', series: series_for(name: 'request.latency', aggregate_using: :avg, action: params[:controller_action]) %>
|
14
|
+
|
15
|
+
<%#= render 'chart', name: "Latency Breakdown", series: @latency_composition, type: 'area' %>
|
16
|
+
<%#= render 'chart', name: 'Errors', series: @errors, type: 'bar', palette: 'palette7' %>
|
17
|
+
</div>
|
18
|
+
<% if params[:controller_action].blank? %>
|
19
|
+
<section class="layout-requests_index-by_controller">
|
20
|
+
<h2>By Controller Action</h2>
|
21
|
+
<div class="table-chart ">
|
22
|
+
<table>
|
23
|
+
<thead>
|
24
|
+
<tr>
|
25
|
+
<th>Controller Action</th>
|
26
|
+
<th>Requests</th>
|
27
|
+
<th>Avg Latency</th>
|
28
|
+
</tr>
|
29
|
+
</thead>
|
30
|
+
<tbody>
|
31
|
+
<% @count_by_controller.each do |series| %>
|
32
|
+
<tr>
|
33
|
+
<td><%= link_to series.labels[:action].classify, requests_path(controller_action: series.labels[:action].to_param), class: 'table-chart-row-action' %></td>
|
34
|
+
<td><%= series.data.dig(0, 1) %></td>
|
35
|
+
<td><%= @latency_by_controller[series.labels[:action]]&.data&.dig(0, 1).to_f.round(2) %> ms</td>
|
36
|
+
</tr>
|
37
|
+
<% end %>
|
38
|
+
|
39
|
+
<!-- More people... -->
|
40
|
+
</tbody>
|
41
|
+
</table>
|
42
|
+
</div>
|
43
|
+
</section>
|
44
|
+
<% end %>
|
45
|
+
<section class="layout-requests_index-events">
|
46
|
+
<h2>Recent Requests</h2>
|
47
|
+
|
48
|
+
|
49
|
+
<%= render 'events_table', events: @events, fields: [
|
50
|
+
:time, :http_method, :action, :route_pattern, :duration, :status,
|
51
|
+
], formatters: {
|
52
|
+
time: ->(time, event) { link_to Time.at(time).to_fs('%H:%M:%S'), request_path(event.request_id) },
|
53
|
+
} %>
|
54
|
+
</section>
|
55
|
+
|
56
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<% content_for :title do %>
|
2
|
+
<span style="font-weight:200"><%= @request.http_method %></span> <%= @request.name.classify %>
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" height="18">
|
4
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3" />
|
5
|
+
</svg>
|
6
|
+
<span class="status-<%= 'success' if @request.status.to_s.start_with?('2') %>"><%= @request.status %> <%= Rack::Utils::HTTP_STATUS_CODES[@request.status] %></span>
|
7
|
+
<br>
|
8
|
+
<span style="font-size: 1rem; font-weight: 200;"><%= time_ago_in_words(Time.at(@request.time)) %> ago
|
9
|
+
| <%= @request.duration.round(2) %>ms
|
10
|
+
| <%= @request.path %></span>
|
11
|
+
<% end %>
|
12
|
+
<% content_for(:hide_duration, 'true') %>
|
13
|
+
|
14
|
+
<% content_for(:main_css_class, 'layout-events-breakdown') %>
|
15
|
+
<%= render 'trace', model: @request, events: @events %>
|
16
|
+
|
data/config/routes.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative './models/mail_delivery'
|
2
|
+
module RailsObservatory
|
3
|
+
class ActionMailerSubscriber < ActiveSupport::Subscriber
|
4
|
+
attach_to :action_mailer
|
5
|
+
|
6
|
+
def deliver(event)
|
7
|
+
event.payload => {mail:, mailer:, to:, from:, subject:, message_id:}
|
8
|
+
MailDelivery.new(mail:, mailer:, to:, from:, subject:, message_id:, time: Time.now.to_f, duration: event.duration).save
|
9
|
+
|
10
|
+
TimeSeries.record_occurrence("mailer.delivery_count", labels: {mailer:})
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'redis-client'
|
2
|
+
|
3
|
+
require_relative './action_mailer_subscriber'
|
4
|
+
module RailsObservatory
|
5
|
+
class Engine < ::Rails::Engine
|
6
|
+
isolate_namespace RailsObservatory
|
7
|
+
|
8
|
+
# This middleware is specific to the engine and will not be included in the application stack
|
9
|
+
middleware.use(Rack::Static, urls: ["/assets"], root: config.root.join("public"))
|
10
|
+
|
11
|
+
config.rails_observatory = ActiveSupport::OrderedOptions.new
|
12
|
+
config.rails_observatory.redis = { host: "localhost", port: 6379, db: 0, pool_size: ENV["RAILS_MAX_THREADS"] || 3 }
|
13
|
+
|
14
|
+
initializer "rails_observatory.redis" do |app|
|
15
|
+
require_relative './redis/logging_middleware'
|
16
|
+
require_relative './redis/redis_client_instrumentation'
|
17
|
+
app.config.rails_observatory.redis => pool_size:, **redis_config
|
18
|
+
redis_config = RedisClient.config(**redis_config.merge(middlewares: [RedisClientInstrumentation]))
|
19
|
+
app.config.rails_observatory.redis = redis_config.new_pool(timeout: 0.5, size: pool_size)
|
20
|
+
end
|
21
|
+
|
22
|
+
initializer "rails_observatory.middleware" do |app|
|
23
|
+
require_relative './middleware'
|
24
|
+
|
25
|
+
# Middleware is not instrumented UNLESS there's a subscriber listening.
|
26
|
+
# By instantiating the collector, we ensure that the InstrumentationProxy is used
|
27
|
+
EventCollector.instance
|
28
|
+
|
29
|
+
app.middleware.unshift(Middleware)
|
30
|
+
end
|
31
|
+
|
32
|
+
initializer "rails_observatory.active_job_instrumentation" do
|
33
|
+
require_relative './models/job_trace'
|
34
|
+
ActiveSupport.on_load(:active_job) do |active_job|
|
35
|
+
require_relative './railties/active_job_instrumentation'
|
36
|
+
active_job.include(Railties::ActiveJobInstrumentation)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
initializer "rails_observatory.logger" do |app|
|
41
|
+
require_relative './log_collector'
|
42
|
+
Rails.logger.broadcast_to(LogCollector.new)
|
43
|
+
end
|
44
|
+
|
45
|
+
initializer "rails_observatory.mailer_instrumentation" do |app|
|
46
|
+
config.action_mailer.preview_paths << "#{config.root}/lib/rails_observatory/mailer_previews"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module RailsObservatory
|
2
|
+
class EventCollector
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
COLLECTOR_LIST_KEY = :ro_collector
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@subscriber ||= ActiveSupport::Notifications.subscribe(/\A[^!]/, self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(event)
|
12
|
+
return if ActiveSupport::IsolatedExecutionState[COLLECTOR_LIST_KEY].blank?
|
13
|
+
|
14
|
+
ActiveSupport::IsolatedExecutionState[COLLECTOR_LIST_KEY].each do |key|
|
15
|
+
if (events = ActiveSupport::IsolatedExecutionState[key])
|
16
|
+
events << event
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate_collector_key
|
23
|
+
"collector:#{Object.new.object_id}"
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def collect_events
|
28
|
+
events = []
|
29
|
+
key = generate_collector_key
|
30
|
+
ActiveSupport::IsolatedExecutionState[COLLECTOR_LIST_KEY] ||= []
|
31
|
+
ActiveSupport::IsolatedExecutionState[COLLECTOR_LIST_KEY] << key
|
32
|
+
ActiveSupport::IsolatedExecutionState[key] = events
|
33
|
+
yield
|
34
|
+
events
|
35
|
+
rescue Exception => e
|
36
|
+
e.instance_variable_set(:@_trace_events, events)
|
37
|
+
raise
|
38
|
+
ensure
|
39
|
+
ActiveSupport::IsolatedExecutionState[COLLECTOR_LIST_KEY].delete(key)
|
40
|
+
ActiveSupport::IsolatedExecutionState.delete(key)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module RailsObservatory
|
2
|
+
class LogCollector < ::Logger
|
3
|
+
|
4
|
+
KEY = :observatory_logs
|
5
|
+
|
6
|
+
def initialize(*args, **kwargs)
|
7
|
+
_, *rest = args
|
8
|
+
super(nil, *rest, **kwargs)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.collect_logs
|
12
|
+
logs = []
|
13
|
+
ActiveSupport::IsolatedExecutionState[KEY] = logs
|
14
|
+
yield
|
15
|
+
logs
|
16
|
+
ensure
|
17
|
+
ActiveSupport::IsolatedExecutionState.delete(KEY)
|
18
|
+
end
|
19
|
+
|
20
|
+
def add(severity, message = nil, progname = nil, &block)
|
21
|
+
if (logs = ActiveSupport::IsolatedExecutionState[KEY])
|
22
|
+
severity ||= UNKNOWN
|
23
|
+
return true if severity < level
|
24
|
+
progname = @progname if progname.nil?
|
25
|
+
if message.nil?
|
26
|
+
if block_given?
|
27
|
+
message = yield
|
28
|
+
else
|
29
|
+
message = progname
|
30
|
+
progname = @progname
|
31
|
+
end
|
32
|
+
end
|
33
|
+
logs << { severity:, message:, progname:, time: Time.now.to_f }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
alias log add
|
38
|
+
|
39
|
+
def <<(message)
|
40
|
+
if (logs = ActiveSupport::IsolatedExecutionState[KEY])
|
41
|
+
logs << { severity: UNKNOWN, message:, progname: nil, time: Time.now.to_f }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# Preview all emails at http://localhost:3000/rails/mailers/new_user_mailer
|
2
|
+
class DeliveredMailPreview < ActionMailer::Preview
|
3
|
+
|
4
|
+
def preview
|
5
|
+
mail_delivery = RailsObservatory::MailDelivery.find(params[:message_id])
|
6
|
+
Mail.new(mail_delivery.mail)
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/notifications"
|
4
|
+
require_relative './event_collector'
|
5
|
+
require_relative './models/request_trace'
|
6
|
+
require_relative './models/error'
|
7
|
+
require_relative './serializers/serializer'
|
8
|
+
require 'zlib'
|
9
|
+
require 'benchmark'
|
10
|
+
|
11
|
+
module RailsObservatory
|
12
|
+
class Middleware
|
13
|
+
|
14
|
+
def initialize(app)
|
15
|
+
@app = app
|
16
|
+
end
|
17
|
+
|
18
|
+
def collect_events_and_logs
|
19
|
+
logs = []
|
20
|
+
events = EventCollector.instance.collect_events do
|
21
|
+
logs = LogCollector.collect_logs do
|
22
|
+
yield
|
23
|
+
end
|
24
|
+
end
|
25
|
+
[events, logs]
|
26
|
+
end
|
27
|
+
|
28
|
+
def call(env)
|
29
|
+
start_at = Time.now
|
30
|
+
request = ActionDispatch::Request.new(env)
|
31
|
+
|
32
|
+
start_at_mono = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
33
|
+
response = nil
|
34
|
+
events, logs = collect_events_and_logs do
|
35
|
+
response = @app.call(env)
|
36
|
+
end
|
37
|
+
|
38
|
+
controller_action = "#{request.params[:controller]}##{request.params[:action]}"
|
39
|
+
|
40
|
+
if (event = events.find { _1.name == 'process_action.action_controller' && _1.payload[:exception_object] })
|
41
|
+
error = Error.new(exception: event.payload[:exception_object], location: controller_action, time: Time.now)
|
42
|
+
error.save
|
43
|
+
TimeSeries.record_occurrence("error.count", labels: { fingerprint: error.fingerprint })
|
44
|
+
end
|
45
|
+
|
46
|
+
return response if request.params[:controller].blank? || request.params[:controller] =~ /rails_observatory/
|
47
|
+
|
48
|
+
status, headers, body = response
|
49
|
+
body = ::Rack::BodyProxy.new(body) do
|
50
|
+
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_at_mono)
|
51
|
+
RequestTrace.new(
|
52
|
+
request_id: request.request_id,
|
53
|
+
status:,
|
54
|
+
http_method: request.method,
|
55
|
+
route_pattern: request.route_uri_pattern,
|
56
|
+
action: controller_action,
|
57
|
+
error: events.any? { _1.payload[:exception] },
|
58
|
+
format: request.format,
|
59
|
+
duration:,
|
60
|
+
time: start_at.to_f,
|
61
|
+
path: request.path,
|
62
|
+
events: events.map { Serializer.serialize(_1) },
|
63
|
+
logs:
|
64
|
+
).save
|
65
|
+
labels = { action: controller_action, format: request.format, status:, http_method: request.method }
|
66
|
+
TimeSeries.record_occurrence("request.count", labels:)
|
67
|
+
TimeSeries.record_occurrence("request.error_count", labels:) if status >= 500
|
68
|
+
TimeSeries.record_timing("request.latency", duration, labels:)
|
69
|
+
rescue => e
|
70
|
+
puts e
|
71
|
+
puts e.backtrace.join("\n")
|
72
|
+
end
|
73
|
+
|
74
|
+
[status, headers, body]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|