ductwork 0.11.2 → 0.13.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.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductwork
4
+ class StepErrorsController < Ductwork::ApplicationController
5
+ def index
6
+ @step_errors = query_step_errors
7
+ @klasses = Ductwork::Result
8
+ .failure
9
+ .joins(execution: { job: :step })
10
+ .group("ductwork_steps.klass")
11
+ .pluck("ductwork_steps.klass")
12
+ end
13
+
14
+ private
15
+
16
+ def query_step_errors
17
+ Ductwork::Result
18
+ .failure
19
+ .then(&method(:filter_by_step_klass))
20
+ .then(&method(:paginate))
21
+ .includes(execution: { job: :step })
22
+ .order(created_at: :desc)
23
+ end
24
+
25
+ def filter_by_step_klass(relation)
26
+ if params[:klass].present?
27
+ relation
28
+ .joins(execution: { job: :step })
29
+ .where(ductwork_steps: { klass: params[:klass] })
30
+ else
31
+ relation
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductwork
4
+ module ApplicationHelper
5
+ def formatted_time_distance(started_at, completed_at)
6
+ elapsed = completed_at - started_at
7
+ hours = (elapsed / 3600).floor
8
+ minutes = ((elapsed % 3600) / 60).floor
9
+ seconds = (elapsed % 60).round(2)
10
+ hours_string = hours.zero? ? "" : "#{hours}h "
11
+ minutes_string = minutes.zero? ? "" : "#{minutes}m "
12
+ seconds_string = seconds.zero? ? "" : "#{seconds}s"
13
+
14
+ "#{hours_string}#{minutes_string}#{seconds_string}"
15
+ end
16
+
17
+ def first_page?
18
+ params[:page].to_i.zero?
19
+ end
20
+
21
+ def next_page_path
22
+ next_page = params[:page].to_i + 1
23
+ next_params = params
24
+ .permit(:controller, :action, :klass, :status, :page)
25
+ .merge(page: next_page)
26
+
27
+ url_for(**next_params)
28
+ end
29
+
30
+ def previous_page_path
31
+ previous_page = params[:page].to_i - 1
32
+ previous_params = params
33
+ .permit(:controller, :action, :klass, :status, :page)
34
+ .merge(page: previous_page)
35
+
36
+ url_for(**previous_params)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,136 @@
1
+ <div>
2
+ <div class="grid metrics">
3
+ <%= link_to(pipelines_path(status: "completed"), class: "stealthy-link") do %>
4
+ <article>
5
+ <div class="metric-heading">Completed Pipelines</div>
6
+ <h1 class="metric-value"><%= number_with_delimiter(@metrics[:completed]) %></h1>
7
+ </article>
8
+ <% end %>
9
+
10
+ <%= link_to(pipelines_path(status: "in_progress"), class: "stealthy-link") do %>
11
+ <article>
12
+ <div class="metric-heading">In-Progress Pipelines</div>
13
+ <h1 class="metric-value"><%= number_with_delimiter(@metrics[:in_progress]) %></h1>
14
+ </article>
15
+ <% end %>
16
+
17
+ <%= link_to(pipelines_path(status: "halted"), class: "stealthy-link") do %>
18
+ <article>
19
+ <div class="metric-heading">Halted Pipelines</div>
20
+ <h1 class="metric-value"><%= number_with_delimiter(@metrics[:halted]) %></h1>
21
+ </article>
22
+ <% end %>
23
+
24
+ <%= link_to(step_errors_path, class: "stealthy-link") do %>
25
+ <article>
26
+ <div class="metric-heading">Step Errors</div>
27
+ <h1 class="metric-value"><%= @metrics[:step_errors] %></h1>
28
+ </article>
29
+ <% end %>
30
+ </div>
31
+
32
+ <div>
33
+ <h1>All Pipelines</h1>
34
+ <div class="separate-content">
35
+ <div class="filter-group">
36
+ <span class="filter-group-item">Filters:</span>
37
+
38
+ <details class="dropdown filter-group-item">
39
+ <summary>Class</summary>
40
+ <ul>
41
+ <% @klasses.each do |klass| %>
42
+ <li>
43
+ <%= link_to(klass, pipelines_path(klass:)) %>
44
+ </li>
45
+ <% end %>
46
+ </ul>
47
+ </details>
48
+
49
+ <details class="dropdown filter-group-item">
50
+ <summary>Status</summary>
51
+ <ul>
52
+ <% @statuses.each do |status| %>
53
+ <li>
54
+ <%= link_to(status.gsub("_", "-"), pipelines_path(status:)) %>
55
+ </li>
56
+ <% end %>
57
+ </ul>
58
+ </details>
59
+ </div>
60
+
61
+ <div role="group" style="width: auto;">
62
+ <button <%= first_page? && "disabled" %> data-href="<%= previous_page_path %>">
63
+ &larr;
64
+ </button>
65
+ <button data-href="<%= next_page_path %>">
66
+ &rarr;
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ <table class="striped">
72
+ <thead>
73
+ <tr>
74
+ <th>ID</th>
75
+ <th>Class</th>
76
+ <th>Status</th>
77
+ <th>Errors</th>
78
+ <th>Started At</th>
79
+ <th>Runtime</th>
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ <% @pipelines.each do |pipeline| %>
84
+ <tr class="row-link" data-href="<%= pipeline_path(pipeline.id) %>">
85
+ <td>
86
+ <code>
87
+ <%= pipeline.id %>
88
+ </code>
89
+ </td>
90
+ <td>
91
+ <code>
92
+ <%= pipeline.klass %>
93
+ </code>
94
+ </td>
95
+ <td>
96
+ <div class="status-pill <%= pipeline.status %>">
97
+ <%= pipeline.status.gsub("_", "-") %>
98
+ </div>
99
+ </td>
100
+ <td>
101
+ <%
102
+ count = Ductwork::Result
103
+ .joins(execution: { job: { step: :pipeline }})
104
+ .failure
105
+ .where(ductwork_pipelines: { id: pipeline.id })
106
+ .count
107
+ %>
108
+ <% if count.zero? %>
109
+ 0
110
+ <% else %>
111
+ <div class="errors">
112
+ <%= image_tag("ductwork/octagon-x.svg", class: "icon") %>
113
+ <%= count %>
114
+ </div>
115
+ <% end %>
116
+ </td>
117
+ <td>
118
+ <div class="tight-timestamp">
119
+ <%= pipeline.started_at.iso8601(3) %>
120
+ </div>
121
+ </td>
122
+ <td>
123
+ <% if pipeline.completed_at.nil? %>
124
+ <div data-started-at=<%= pipeline.started_at.iso8601 %>>
125
+ <span id="elapsed-timer">0h 0m 0s</span>
126
+ </div>
127
+ <% else %>
128
+ <%= formatted_time_distance(pipeline.started_at, pipeline.completed_at) %>
129
+ <% end %>
130
+ </td>
131
+ </tr>
132
+ <% end %>
133
+ </tbody>
134
+ </table>
135
+ </div>
136
+ </div>
@@ -0,0 +1,105 @@
1
+ <div>
2
+ <h1>All Pipelines</h1>
3
+
4
+ <div class="separate-content">
5
+ <div class="filter-group">
6
+ <span class="filter-group-item">Filters:</span>
7
+
8
+ <details class="dropdown filter-group-item">
9
+ <summary><%= params[:klass] || "Class" %></summary>
10
+ <ul>
11
+ <% @klasses.each do |klass| %>
12
+ <li>
13
+ <%= link_to(klass, pipelines_path(params.permit(:status).merge(klass: klass))) %>
14
+ </li>
15
+ <% end %>
16
+ </ul>
17
+ </details>
18
+
19
+ <details class="dropdown filter-group-item">
20
+ <summary><%= params[:status]&.gsub("_", "-") || "Status" %></summary>
21
+ <ul>
22
+ <% @statuses.each do |status| %>
23
+ <li>
24
+ <%= link_to(status.gsub("_", "-"), pipelines_path(params.permit(:klass).merge(status:))) %>
25
+ </li>
26
+ <% end %>
27
+ </ul>
28
+ </details>
29
+
30
+ <%= link_to("Clear", pipelines_path) %>
31
+ </div>
32
+
33
+ <div role="group" style="width: auto;">
34
+ <button <%= first_page? && "disabled" %> data-href="<%= previous_page_path %>">
35
+ <%= link_to(previous_page_path, class: "stealthy-link") do %>
36
+ &larr;
37
+ <% end %>
38
+ </button>
39
+ <button data-href="<%= next_page_path %>">
40
+ &rarr;
41
+ </button>
42
+ </div>
43
+ </div>
44
+
45
+ <table class="striped">
46
+ <thead>
47
+ <tr>
48
+ <th>ID</th>
49
+ <th>Class</th>
50
+ <th>Status</th>
51
+ <th>Errors</th>
52
+ <th>Started At</th>
53
+ <th>Runtime</th>
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ <% @pipelines.each do |pipeline| %>
58
+ <tr class="row-link" data-href="<%= pipeline_path(pipeline.id) %>">
59
+ <td>
60
+ <code><%= pipeline.id %></code>
61
+ </td>
62
+ <td>
63
+ <code><%= pipeline.klass %></code>
64
+ </td>
65
+ <td>
66
+ <div class="status-pill <%= pipeline.status %>">
67
+ <%= pipeline.status.gsub("_", "-") %>
68
+ </div>
69
+ </td>
70
+ <td>
71
+ <%
72
+ count = Ductwork::Result
73
+ .joins(execution: { job: { step: :pipeline }})
74
+ .failure
75
+ .where(ductwork_pipelines: { id: pipeline.id })
76
+ .count
77
+ %>
78
+ <% if count.zero? %>
79
+ 0
80
+ <% else %>
81
+ <div class="errors">
82
+ <%= image_tag("ductwork/octagon-x.svg", class: "icon") %>
83
+ <%= count %>
84
+ </div>
85
+ <% end %>
86
+ </td>
87
+ <td>
88
+ <div class="tight-timestamp">
89
+ <%= pipeline.started_at.iso8601(3) %>
90
+ </div>
91
+ </td>
92
+ <td>
93
+ <% if pipeline.completed_at.nil? %>
94
+ <div data-started-at=<%= pipeline.started_at.iso8601 %>>
95
+ <span id="elapsed-timer">0h 0m 0s</span>
96
+ </div>
97
+ <% else %>
98
+ <%= formatted_time_distance(pipeline.started_at, pipeline.completed_at) %>
99
+ <% end %>
100
+ </td>
101
+ </tr>
102
+ <% end %>
103
+ </tbody>
104
+ </table>
105
+ </div>
@@ -0,0 +1,266 @@
1
+ <div>
2
+ <div class="separate-content">
3
+ <h1>
4
+ Pipeline <code><%= @pipeline.id %></code>
5
+ </h1>
6
+
7
+ <h2>
8
+ <span class="status-pill big-pill <%= @pipeline.status %>">
9
+ <%= @pipeline.status.gsub("_", "-") %>
10
+ </span>
11
+ </h2>
12
+ </div>
13
+
14
+ <article>
15
+ <div class="separate-content">
16
+ <div>
17
+ <div class="metric-heading">
18
+ <label>ID</label>
19
+ <code><%= @pipeline.id %></code>
20
+ </div>
21
+ <div>
22
+ <label>Class</label>
23
+ <code>
24
+ <%= link_to(@pipeline.klass, pipelines_path(klass: @pipeline.klass)) %>
25
+ </code>
26
+ </div>
27
+ </div>
28
+
29
+ <div>
30
+ <div class="metric-heading">
31
+ <label>Triggered At</label>
32
+ <span>
33
+ <%= @pipeline.triggered_at.iso8601(3) %>
34
+ </span>
35
+ </div>
36
+ <div>
37
+ <label>Step Count</label>
38
+ <span>
39
+ <%= @pipeline.steps.count %>
40
+ </span>
41
+ </div>
42
+ </div>
43
+
44
+ <div>
45
+ <div class="metric-heading">
46
+ <label>Last Advanced At</label>
47
+ <span>
48
+ <%= @pipeline.last_advanced_at.iso8601(3) %>
49
+ </span>
50
+ </div>
51
+ <div>
52
+ <label>Completed At</label>
53
+ <span>
54
+ <%= @pipeline.completed_at&.iso8601(3) %>
55
+ </span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <div style="margin-top: 1rem;">
61
+ <label>Definition</label>
62
+ <code>
63
+ <%= @pipeline.parsed_definition %>
64
+ </code>
65
+ </div>
66
+ </article>
67
+
68
+ <h3>Steps</h3>
69
+
70
+ <div class="separate-content">
71
+ <div class="filter-group">
72
+ <span class="filter-group-item">Filters:</span>
73
+
74
+ <details class="dropdown filter-group-item">
75
+ <summary><%= params[:klass] || "Class" %></summary>
76
+ <ul>
77
+ <% @klasses.each do |klass| %>
78
+ <li>
79
+ <%= link_to(klass, pipeline_path(@pipeline.id, **params.permit(:status).merge(klass: klass))) %>
80
+ </li>
81
+ <% end %>
82
+ </ul>
83
+ </details>
84
+
85
+ <details class="dropdown filter-group-item">
86
+ <summary><%= params[:status]&.gsub("_", "-") || "Status" %></summary>
87
+ <ul>
88
+ <% @statuses.each do |status| %>
89
+ <li>
90
+ <%= link_to(status.gsub("_", "-"), pipeline_path(@pipeline.id, **params.permit(:klass).merge(status:))) %>
91
+ </li>
92
+ <% end %>
93
+ </ul>
94
+ </details>
95
+
96
+ <%= link_to("Clear", pipeline_path(@pipeline.id)) %>
97
+ </div>
98
+
99
+ <div role="group" style="width: auto;">
100
+ <button <%= first_page? && "disabled" %> data-href="<%= previous_page_path %>">
101
+ <%= link_to(previous_page_path, class: "stealthy-link") do %>
102
+ &larr;
103
+ <% end %>
104
+ </button>
105
+ <button data-href="<%= next_page_path %>">
106
+ &rarr;
107
+ </button>
108
+ </div>
109
+ </div>
110
+
111
+ <article style="margin-bottom: 0px;">
112
+ <div>
113
+ <% @steps.each do |step| %>
114
+ <details>
115
+ <summary class="details-grid">
116
+ <div class="grid">
117
+ <div>
118
+ <label>ID</label>
119
+ <code>
120
+ <%= step.id %>
121
+ </code>
122
+ </div>
123
+
124
+ <div>
125
+ <label>Status</label>
126
+ <div class="status-pill <%= step.status %>">
127
+ <%= step.status.gsub("_", "-") %>
128
+ </div>
129
+ </div>
130
+
131
+ <div>
132
+ <label>Transition</label>
133
+ <code>
134
+ <%= step.to_transition == "default" ? "chain" : step.to_transition %>
135
+ </code>
136
+ </div>
137
+
138
+ <div>
139
+ <label>Errors</label>
140
+ <% if (step.job.executions.count - 1).zero? %>
141
+ 0
142
+ <% else %>
143
+ <div class="errors">
144
+ <%= image_tag("ductwork/octagon-x.svg") %>
145
+ <%= step.job.executions.count - 1 %>
146
+ </div>
147
+ <% end %>
148
+ </div>
149
+
150
+ <div>
151
+ <label>Total Runtime</label>
152
+ <% if step.completed_at.nil? %>
153
+ <div data-started-at=<%= step.started_at.iso8601 %>>
154
+ <span id="elapsed-timer">0h 0m 0s</span>
155
+ </div>
156
+ <% else %>
157
+ <%= formatted_time_distance(step.started_at, step.completed_at) %>
158
+ <% end %>
159
+ </div>
160
+ </div>
161
+ </summary>
162
+
163
+ <div>
164
+ <div class="grid">
165
+ <div>
166
+ <label>Node</label>
167
+ <code>
168
+ <%= step.node %>
169
+ </code>
170
+ </div>
171
+
172
+ <div>
173
+ <label>Class</label>
174
+ <code>
175
+ <%= step.klass %>
176
+ </code>
177
+ </div>
178
+
179
+ <div>
180
+ <label>Started At</label>
181
+ <div class="tight-timestamp">
182
+ <%= step.started_at.iso8601(3) %>
183
+ </div>
184
+ </div>
185
+
186
+ <div>
187
+ <label>Completed At</label>
188
+ <div class="tight-timestamp">
189
+ <%= step.completed_at&.iso8601(3) %>
190
+ </div>
191
+ </div>
192
+
193
+ </div>
194
+
195
+ <div style="margin-top: 1rem;">
196
+ <label>Executions</label>
197
+ <table class="small-table">
198
+ <thead>
199
+ <tr>
200
+ <th>ID</th>
201
+ <th>Attempt</th>
202
+ <th>Available At</th>
203
+ <th>Claimed At</th>
204
+ <th>Ran At</th>
205
+ <th>Completed At</th>
206
+ <th>Error</th>
207
+ </tr>
208
+ </thead>
209
+
210
+ <tbody>
211
+ <% step.job.executions.each do |execution| %>
212
+ <tr class="not-clickable">
213
+ <td>
214
+ <code>
215
+ <%= execution.id %>
216
+ </code>
217
+ </td>
218
+ <td><%= execution.retry_count + 1 %></td>
219
+ <td><%= execution.started_at.strftime("%H:%M:%S.%3N") %></td>
220
+ <td><%= execution.availability.completed_at&.strftime("%H:%M:%S.%3N") %></td>
221
+ <td><%= execution.run&.started_at&.strftime("%H:%M:%S.%3N") %></td>
222
+ <td><%= execution.completed_at&.strftime("%H:%M:%S.%3N") %></td>
223
+ <td>
224
+ <% if execution.result.present? && execution.result.failure? %>
225
+ <button data-open-modal="modal-<%= execution.result.id %>" class="tight-timestamp secondary">
226
+ Open
227
+ </button>
228
+
229
+ <dialog id="modal-<%= execution.result.id %>">
230
+ <article>
231
+ <header>
232
+ <button aria-label="Close" rel="prev" data-close-modal="modal-<%= execution.result.id %>"></button>
233
+ <p>
234
+ <strong>Error Backtrace</strong>
235
+ </p>
236
+ </header>
237
+ <div>
238
+ <h2>
239
+ <%= execution.result.error_klass %>
240
+ </h2>
241
+ <h5>
242
+ <%= execution.result.error_message %>
243
+ </h5>
244
+ <code>
245
+ <%= execution.result.error_backtrace.split("\n").each do |line| %>
246
+ <div><%= line %></div>
247
+ <% end %>
248
+ </code>
249
+ </div>
250
+ </article>
251
+ </dialog>
252
+ <% end %>
253
+ </td>
254
+ </tr>
255
+ <% end %>
256
+ </tbody>
257
+ </table>
258
+ </div>
259
+ </div>
260
+ </details>
261
+
262
+ <hr/>
263
+ <% end %>
264
+ </div>
265
+ </article>
266
+ </div>
@@ -0,0 +1,101 @@
1
+ <div>
2
+ <h3>Step Errors</h3>
3
+
4
+ <div class="separate-content">
5
+ <div class="filter-group">
6
+ <span class="filter-group-item">Filters:</span>
7
+
8
+ <details class="dropdown filter-group-item">
9
+ <summary><%= params[:klass] || "Step Class" %></summary>
10
+ <ul>
11
+ <% @klasses.each do |klass| %>
12
+ <li>
13
+ <%= link_to(klass, step_errors_path(**params.permit(:status).merge(klass: klass))) %>
14
+ </li>
15
+ <% end %>
16
+ </ul>
17
+ </details>
18
+
19
+ <%= link_to("Clear", step_errors_path) %>
20
+ </div>
21
+
22
+ <div role="group" style="width: auto;">
23
+ <button <%= first_page? && "disabled" %> data-href="<%= previous_page_path %>">
24
+ <%= link_to(previous_page_path, class: "stealthy-link") do %>
25
+ &larr;
26
+ <% end %>
27
+ </button>
28
+ <button data-href="<%= next_page_path %>">
29
+ &rarr;
30
+ </button>
31
+ </div>
32
+ </div>
33
+ <table class="striped">
34
+ <thead>
35
+ <tr>
36
+ <th>Step ID</th>
37
+ <th>Step Class</th>
38
+ <th>Pipeline ID</th>
39
+ <th>Error Message</th>
40
+ <th>Error Backtrace</th>
41
+ </tr>
42
+ </thead>
43
+
44
+ <tbody>
45
+ <% @step_errors.each do |result| %>
46
+ <tr class="not-clickable">
47
+ <td>
48
+ <code>
49
+ <%= result.execution.job.step.id %>
50
+ </code>
51
+ </td>
52
+ <td>
53
+ <code>
54
+ <%= result.execution.job.step.klass %>
55
+ </code>
56
+ </td>
57
+ <td>
58
+ <code>
59
+ <%= link_to(result.execution.job.step.pipeline_id, pipeline_path(result.execution.job.step.pipeline_id)) %>
60
+ </code>
61
+ </td>
62
+ <td>
63
+ <code>
64
+ <%= [result.error_klass, result.error_message].join(": ") %>
65
+ </code>
66
+ </td>
67
+ <td>
68
+ <button data-open-modal="modal-<%= result.id %>" class="tight-timestamp secondary">
69
+ Open
70
+ </button>
71
+
72
+ <dialog id="modal-<%= result.id %>">
73
+ <article>
74
+ <header>
75
+ <button aria-label="Close" rel="prev" data-close-modal="modal-<%= result.id %>"></button>
76
+ <p>
77
+ <strong>Error Backtrace</strong>
78
+ </p>
79
+ </header>
80
+ <div>
81
+ <h2>
82
+ <%= result.error_klass %>
83
+ </h2>
84
+ <h5>
85
+ <%= result.error_message %>
86
+ </h5>
87
+ <code>
88
+ <%= result.error_backtrace.split("\n").each do |line| %>
89
+ <div><%= line %></div>
90
+ <% end %>
91
+ </code>
92
+ </div>
93
+ </article>
94
+ </dialog>
95
+
96
+ </td>
97
+ </tr>
98
+ <% end %>
99
+ </tbody>
100
+ </table>
101
+ </div>