eyeloupe 0.3.1 → 0.4.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +2 -9
- data/app/controllers/concerns/eyeloupe/searchable.rb +5 -1
- data/app/controllers/eyeloupe/jobs_controller.rb +22 -0
- data/app/helpers/eyeloupe/jobs_helper.rb +4 -0
- data/app/helpers/eyeloupe/request_helper.rb +32 -0
- data/app/models/eyeloupe/job.rb +7 -0
- data/app/views/eyeloupe/in_requests/show.html.erb +2 -2
- data/app/views/eyeloupe/jobs/_frame.html.erb +46 -0
- data/app/views/eyeloupe/jobs/index.html.erb +18 -0
- data/app/views/eyeloupe/jobs/show.html.erb +63 -0
- data/app/views/eyeloupe/out_requests/show.html.erb +2 -2
- data/app/views/eyeloupe/shared/_job_status.html.erb +21 -0
- data/app/views/layouts/eyeloupe/application.html.erb +20 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20230827161224_create_eyeloupe_jobs.rb +19 -0
- data/lib/eyeloupe/engine.rb +24 -0
- data/lib/eyeloupe/processors/job.rb +113 -0
- data/lib/eyeloupe/version.rb +1 -1
- data/lib/eyeloupe.rb +1 -0
- metadata +26 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bff4826ab05209a18e73946c70c010ba1c6196514c639fc3066820ac483602ba
|
|
4
|
+
data.tar.gz: c6f770934433bd5f6592021227a3caf1214a6f0a7e828141b1955c6e9eb7121f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad01737f9bd77b93a765fccbe76e44738d87975f8e9f0abef12942330bb3b655e45872c894c360b0cf454d334a3b283cfec97c7eadd2f4f4fb8fab44eab98cc5
|
|
7
|
+
data.tar.gz: bbee31e3b76dfd55e642b8577780e78058ecdf47ef2bf09acc2c216687d0c93fb3c2a4a513171afd0877a1f04a363bfa4869db27d82894a7a879f99fc202c23e
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<img src="app/assets/images/eyeloupe/logo.png" width=120 alt="Logo" >
|
|
13
13
|
</a>
|
|
14
14
|
|
|
15
|
-
<h3 align="center">Eyeloupe
|
|
15
|
+
<h3 align="center">Eyeloupe</h3>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
18
|
The elegant Rails debug assistant. AI powered.
|
|
@@ -141,13 +141,6 @@ Eyeloupe is not a performance-oriented tool, the request time is the same you ca
|
|
|
141
141
|
|
|
142
142
|
Yes, Eyeloupe is inspired by Laravel Telescope. A lot of people coming from Laravel are missing Telescope or looking for something similar, so Eyeloupe is here to fill this gap.
|
|
143
143
|
|
|
144
|
-
## Roadmap
|
|
145
|
-
|
|
146
|
-
- [x] Exceptions - Track all the exceptions thrown by your application
|
|
147
|
-
- [x] AI assistant - Use OpenAI API to help you to solve your exceptions
|
|
148
|
-
- [ ] Custom links to the menu - To access all of your debug tool in one place (Sidekiq web, Mailhog, etc.)
|
|
149
|
-
- [ ] Refactoring / clean code - To make the code more readable and maintainable
|
|
150
|
-
|
|
151
144
|
## Contributing
|
|
152
145
|
Contributions are what makes the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
|
153
146
|
|
|
@@ -165,7 +158,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
165
158
|
|
|
166
159
|
## Contact
|
|
167
160
|
|
|
168
|
-
[](https://x.com/alxlion_)
|
|
169
162
|
|
|
170
163
|
Project Link: [https://github.com/alxlion/eyeloupe](https://github.com/alxlion/eyeloupe)
|
|
171
164
|
|
|
@@ -4,6 +4,9 @@ module Eyeloupe
|
|
|
4
4
|
module Searchable
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
|
+
path_models = %w[InRequest OutRequest]
|
|
8
|
+
name_models = %w[Job]
|
|
9
|
+
|
|
7
10
|
included do
|
|
8
11
|
before_action :set_query, only: [:index]
|
|
9
12
|
end
|
|
@@ -12,7 +15,8 @@ module Eyeloupe
|
|
|
12
15
|
|
|
13
16
|
def set_query
|
|
14
17
|
model = ("Eyeloupe::" + controller_name.classify).constantize
|
|
15
|
-
|
|
18
|
+
where = model.attribute_names.include?("path") ? 'path' : 'classname'
|
|
19
|
+
@query = params[:q].present? ? model.where("#{where} LIKE ?", "%#{params[:q].strip}%").order(created_at: :desc)
|
|
16
20
|
: model.all.order(created_at: :desc)
|
|
17
21
|
end
|
|
18
22
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Eyeloupe
|
|
2
|
+
class JobsController < ApplicationController
|
|
3
|
+
include Searchable
|
|
4
|
+
|
|
5
|
+
before_action :set_job, only: %i[ show ]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@pagy, @jobs = pagy(@query, items: 50)
|
|
9
|
+
|
|
10
|
+
render partial: 'frame' if params[:frame].present?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
# Use callbacks to share common setup or constraints between actions.
|
|
18
|
+
def set_job
|
|
19
|
+
@job = Job.find(params[:id])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eyeloupe
|
|
4
|
+
module RequestHelper
|
|
5
|
+
# @param [Eyeloupe::InRequest, Eyeloupe::OutRequest] request The request object
|
|
6
|
+
# @return [String] The formatted response
|
|
7
|
+
def format_response(request)
|
|
8
|
+
type = request.format.to_s != '*/*' ? request.format.to_s : request&.headers
|
|
9
|
+
format(type, request.response)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param [Eyeloupe::InRequest, Eyeloupe::OutRequest] request The request object
|
|
13
|
+
# @return [String] The formatted payload
|
|
14
|
+
def format_payload(request)
|
|
15
|
+
type = request.format.to_s != '*/*' ? request.format.to_s : request&.headers
|
|
16
|
+
format(type, request.payload)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def format(format, str)
|
|
22
|
+
case format
|
|
23
|
+
when /json/
|
|
24
|
+
JSON.pretty_generate(JSON.parse(str || '{}'))
|
|
25
|
+
when /xml/
|
|
26
|
+
Nokogiri::XML(str || '<></>').to_xml(indent: 2)
|
|
27
|
+
else
|
|
28
|
+
str
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
<dt class="text-base font-medium leading-6 text-gray-900">Payload</dt>
|
|
54
54
|
<dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
|
|
55
55
|
<% if @request.payload.present? %>
|
|
56
|
-
<pre class="p-2"><%= @request
|
|
56
|
+
<pre class="p-2"><%= format_payload @request %></pre>
|
|
57
57
|
<% else %>
|
|
58
58
|
<p class="text-gray-400 p-2">No payload</p>
|
|
59
59
|
<% end %>
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
75
75
|
<dt class="text-base font-medium leading-6 text-gray-900">Response</dt>
|
|
76
76
|
<dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
|
|
77
|
-
<pre class="p-2"><%=
|
|
77
|
+
<pre class="p-2"><%= format_response @request %></pre>
|
|
78
78
|
</dd>
|
|
79
79
|
</div>
|
|
80
80
|
</dl>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<%= turbo_frame_tag "frame" do %>
|
|
2
|
+
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
3
|
+
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
|
4
|
+
|
|
5
|
+
<table class="min-w-full divide-y divide-gray-300">
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th scope="col" class="py-3 pl-4 pr-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500 sm:pl-0">Name</th>
|
|
9
|
+
<th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Queue</th>
|
|
10
|
+
<th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Adapter</th>
|
|
11
|
+
<th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Status</th>
|
|
12
|
+
<th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Retry</th>
|
|
13
|
+
<th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Enqueued at</th>
|
|
14
|
+
<th scope="col" class="relative py-3 pl-3 pr-4 sm:pr-0">
|
|
15
|
+
<span class="sr-only">Details</span>
|
|
16
|
+
</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody class="divide-y divide-gray-200">
|
|
20
|
+
<% @jobs.each do |job| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-base font-medium text-gray-900 sm:pl-0">
|
|
23
|
+
<%= job.classname %>
|
|
24
|
+
</td>
|
|
25
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= job.queue_name %></td>
|
|
26
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= job.adapter %></td>
|
|
27
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500">
|
|
28
|
+
<%= render "eyeloupe/shared/job_status", job: job %>
|
|
29
|
+
</td>
|
|
30
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= job.retry %></td>
|
|
31
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= distance_of_time_in_words(job.created_at, DateTime.now) %></td>
|
|
32
|
+
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-base font-medium sm:pr-0">
|
|
33
|
+
<%= link_to "Details", job_path(job), class: "text-gray-600 hover:text-gray-900", data: {"turbo_frame": "_top"} %>
|
|
34
|
+
</td>
|
|
35
|
+
</tr>
|
|
36
|
+
<% end %>
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
</div>
|
|
40
|
+
<aside class="mt-4 px-4 py-3 flex items-center justify-center sm:px-6" aria-label="Pagination">
|
|
41
|
+
<div class="flex-1 flex justify-center">
|
|
42
|
+
<%== pagy_nav(@pagy) %>
|
|
43
|
+
</div>
|
|
44
|
+
</aside>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div data-controller="eyeloupe--search" class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
|
|
2
|
+
<div class="sm:flex sm:items-center">
|
|
3
|
+
<div class="sm:flex-auto">
|
|
4
|
+
<h1 class="text-xl font-semibold leading-6 text-gray-900">Jobs</h1>
|
|
5
|
+
<p class="mt-2 text-sm text-gray-700">All jobs running in your application</p>
|
|
6
|
+
</div>
|
|
7
|
+
<div>
|
|
8
|
+
<form method="get" data-action="eyeloupe--search#submit">
|
|
9
|
+
<input type="hidden" name="frame" value="frame" />
|
|
10
|
+
<label for="q" class="sr-only">Search</label>
|
|
11
|
+
<input type="text" id="q" name="q" value="<%= params[:q] %>" placeholder="Search for job class" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm" />
|
|
12
|
+
</form>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="mt-8 flow-root">
|
|
16
|
+
<%= turbo_frame_tag "frame", src: jobs_path(frame: true), data: {"eyeloupe--refresh-target": "frame", "eyeloupe--search-target": "frame"} do %><% end %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<div class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
|
|
2
|
+
<div class="px-4 sm:px-0">
|
|
3
|
+
<h3 class="text-xl font-semibold leading-7 text-gray-900">Job details</h3>
|
|
4
|
+
</div>
|
|
5
|
+
<div class="mt-6 border-t border-gray-100">
|
|
6
|
+
<dl class="divide-y divide-gray-100">
|
|
7
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
8
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Enqueued at</dt>
|
|
9
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
10
|
+
<%= @job.created_at.to_formatted_s(:long) %> (<%= distance_of_time_in_words(@job.created_at, DateTime.now) %>)
|
|
11
|
+
</dd>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
14
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Duration</dt>
|
|
15
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
16
|
+
<%= (@job.completed_at - @job.created_at).round %> seconds
|
|
17
|
+
</dd>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
20
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Name</dt>
|
|
21
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @job.classname %></dd>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
24
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Adapter</dt>
|
|
25
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
26
|
+
<%= @job.adapter %>
|
|
27
|
+
</dd>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
30
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Queue</dt>
|
|
31
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @job.queue_name %></dd>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
34
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Job ID</dt>
|
|
35
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
36
|
+
<%= @job.job_id %>
|
|
37
|
+
</dd>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
40
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Retry</dt>
|
|
41
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
42
|
+
<%= @job.retry %>
|
|
43
|
+
</dd>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
46
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Status</dt>
|
|
47
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
48
|
+
<%= render "eyeloupe/shared/job_status", job: @job %>
|
|
49
|
+
</dd>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
52
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Arguments</dt>
|
|
53
|
+
<dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
|
|
54
|
+
<% if @job.args.present? %>
|
|
55
|
+
<pre class="p-2"><%= JSON.pretty_generate(JSON.parse(@job.args || "{}")) %></pre>
|
|
56
|
+
<% else %>
|
|
57
|
+
<p class="text-gray-400 p-2">No args</p>
|
|
58
|
+
<% end %>
|
|
59
|
+
</dd>
|
|
60
|
+
</div>
|
|
61
|
+
</dl>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<dt class="text-base font-medium leading-6 text-gray-900">Payload</dt>
|
|
41
41
|
<dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
|
|
42
42
|
<% if @request.payload.present? %>
|
|
43
|
-
<pre class="p-2"><%= @request
|
|
43
|
+
<pre class="p-2"><%= format_payload @request %></pre>
|
|
44
44
|
<% else %>
|
|
45
45
|
<p class="text-gray-400 p-2">No payload</p>
|
|
46
46
|
<% end %>
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
62
62
|
<dt class="text-base font-medium leading-6 text-gray-900">Response</dt>
|
|
63
63
|
<dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
|
|
64
|
-
<pre class="p-2"><%=
|
|
64
|
+
<pre class="p-2"><%= format_response @request %></pre>
|
|
65
65
|
</dd>
|
|
66
66
|
</div>
|
|
67
67
|
</dl>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<% if job.enqueued? %>
|
|
2
|
+
<span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-base font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
|
|
3
|
+
<%= job.status.capitalize %>
|
|
4
|
+
</span>
|
|
5
|
+
<% elsif job.running? %>
|
|
6
|
+
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-base font-medium text-yellow-800 ring-1 ring-inset ring-yellow-600/20">
|
|
7
|
+
<%= job.status.capitalize %>
|
|
8
|
+
</span>
|
|
9
|
+
<% elsif job.completed? %>
|
|
10
|
+
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-base font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
|
|
11
|
+
<%= job.status.capitalize %>
|
|
12
|
+
</span>
|
|
13
|
+
<% elsif job.failed? %>
|
|
14
|
+
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-base font-medium text-red-700 ring-1 ring-inset ring-red-600/10">
|
|
15
|
+
<%= job.status.capitalize %>
|
|
16
|
+
</span>
|
|
17
|
+
<% else %>
|
|
18
|
+
<span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-base font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
|
|
19
|
+
<%= job.status.capitalize %>
|
|
20
|
+
</span>
|
|
21
|
+
<% end %>
|
|
@@ -84,6 +84,16 @@
|
|
|
84
84
|
Exceptions
|
|
85
85
|
<% end %>
|
|
86
86
|
</li>
|
|
87
|
+
<li>
|
|
88
|
+
<%= link_to jobs_path, class:"#{request.path.include?('/jobs') ? 'bg-gray-200 text-red-500' : ''} hover:bg-gray-200 text-gray-500 group flex gap-x-3 rounded-md p-2 text-base leading-6 font-medium" do %>
|
|
89
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
90
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
|
91
|
+
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
|
92
|
+
<path d="M4 14m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
|
93
|
+
</svg>
|
|
94
|
+
Jobs
|
|
95
|
+
<% end %>
|
|
96
|
+
</li>
|
|
87
97
|
</ul>
|
|
88
98
|
</li>
|
|
89
99
|
</ul>
|
|
@@ -148,6 +158,16 @@
|
|
|
148
158
|
Exceptions
|
|
149
159
|
<% end %>
|
|
150
160
|
</li>
|
|
161
|
+
<li>
|
|
162
|
+
<%= link_to jobs_path, class:"#{request.path.include?('/jobs') ? 'bg-gray-200 text-red-500' : ''} hover:bg-gray-200 text-gray-500 group flex gap-x-3 rounded-md p-2 text-base leading-6 font-medium" do %>
|
|
163
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
164
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
|
165
|
+
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
|
166
|
+
<path d="M4 14m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
|
167
|
+
</svg>
|
|
168
|
+
Jobs
|
|
169
|
+
<% end %>
|
|
170
|
+
</li>
|
|
151
171
|
</ul>
|
|
152
172
|
</li>
|
|
153
173
|
</ul>
|
data/config/routes.rb
CHANGED
|
@@ -5,6 +5,7 @@ Eyeloupe::Engine.routes.draw do
|
|
|
5
5
|
resources :in_requests, only: [:index, :show]
|
|
6
6
|
resources :out_requests, only: [:index, :show]
|
|
7
7
|
resources :exceptions, only: [:index, :show]
|
|
8
|
+
resources :jobs, only: [:index, :show]
|
|
8
9
|
resources :ai_assistant_responses, only: [:show]
|
|
9
10
|
|
|
10
11
|
resource :data, only: [:destroy]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreateEyeloupeJobs < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :eyeloupe_jobs do |t|
|
|
4
|
+
t.string :classname
|
|
5
|
+
t.string :job_id
|
|
6
|
+
t.string :queue_name
|
|
7
|
+
t.string :adapter
|
|
8
|
+
t.integer :status, default: 0
|
|
9
|
+
t.datetime :scheduled_at
|
|
10
|
+
t.datetime :executed_at
|
|
11
|
+
t.datetime :completed_at
|
|
12
|
+
t.integer :retry, default: 0
|
|
13
|
+
t.string :args
|
|
14
|
+
t.timestamps
|
|
15
|
+
|
|
16
|
+
t.index :job_id, unique: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/eyeloupe/engine.rb
CHANGED
|
@@ -15,6 +15,30 @@ module Eyeloupe
|
|
|
15
15
|
initializer 'eyeloupe.active_job' do
|
|
16
16
|
ActiveSupport.on_load(:active_job) do
|
|
17
17
|
include Eyeloupe::Concerns::Rescuable
|
|
18
|
+
|
|
19
|
+
ActiveSupport::Notifications.subscribe("enqueue_at.active_job") do |*args|
|
|
20
|
+
Eyeloupe::Processors::Job.instance.process(ActiveSupport::Notifications::Event.new(*args))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ActiveSupport::Notifications.subscribe("enqueue.active_job") do |*args|
|
|
24
|
+
Eyeloupe::Processors::Job.instance.process(ActiveSupport::Notifications::Event.new(*args))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
ActiveSupport::Notifications.subscribe("perform_start.active_job") do |*args|
|
|
28
|
+
Eyeloupe::Processors::Job.instance.run(ActiveSupport::Notifications::Event.new(*args))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
|
|
32
|
+
Eyeloupe::Processors::Job.instance.complete(ActiveSupport::Notifications::Event.new(*args))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
ActiveSupport::Notifications.subscribe("retry_stopped.active_job") do |*args|
|
|
36
|
+
Eyeloupe::Processors::Job.instance.failed(ActiveSupport::Notifications::Event.new(*args))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
ActiveSupport::Notifications.subscribe("discard.active_job") do |*args|
|
|
40
|
+
Eyeloupe::Processors::Job.instance.discard(ActiveSupport::Notifications::Event.new(*args))
|
|
41
|
+
end
|
|
18
42
|
end
|
|
19
43
|
end
|
|
20
44
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Eyeloupe
|
|
3
|
+
module Processors
|
|
4
|
+
class Job
|
|
5
|
+
include Singleton
|
|
6
|
+
|
|
7
|
+
# @return [Array]
|
|
8
|
+
attr_accessor :subs
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@subs = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [ActiveSupport::Notifications::Event] event The event object
|
|
15
|
+
def process(event)
|
|
16
|
+
job = event.payload[:job]
|
|
17
|
+
|
|
18
|
+
Eyeloupe::Job.create(
|
|
19
|
+
classname: job.class.name,
|
|
20
|
+
job_id: job.job_id,
|
|
21
|
+
queue_name: queue_name(event),
|
|
22
|
+
adapter: adapter_name(event),
|
|
23
|
+
scheduled_at: scheduled_at(event),
|
|
24
|
+
status: :enqueued,
|
|
25
|
+
args: (args_info(job) || {}).to_json
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param [ActiveSupport::Notifications::Event] event The event object
|
|
30
|
+
def run(event)
|
|
31
|
+
job = event.payload[:job]
|
|
32
|
+
|
|
33
|
+
Eyeloupe::Job.where(job_id: job.job_id).update(status: :running, executed_at: Time.now.utc)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param [ActiveSupport::Notifications::Event] event The event object
|
|
37
|
+
def complete(event)
|
|
38
|
+
job = event.payload[:job]
|
|
39
|
+
|
|
40
|
+
existing = Eyeloupe::Job.where(job_id: job.job_id).first
|
|
41
|
+
|
|
42
|
+
if existing&.failed?
|
|
43
|
+
Eyeloupe::Job.where(job_id: job.job_id).update(completed_at: Time.now.utc, retry: existing.retry + 1)
|
|
44
|
+
else
|
|
45
|
+
Eyeloupe::Job.where(job_id: job.job_id).update(
|
|
46
|
+
status: :completed,
|
|
47
|
+
completed_at: Time.now.utc,
|
|
48
|
+
retry: (job.executions.zero? ? 1 : job.executions) - 1
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param [ActiveSupport::Notifications::Event] event The event object
|
|
54
|
+
def failed(event)
|
|
55
|
+
job = event.payload[:job]
|
|
56
|
+
|
|
57
|
+
Eyeloupe::Job.where(job_id: job.job_id).update(status: :failed)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param [ActiveSupport::Notifications::Event] event The event object
|
|
61
|
+
def discard(event)
|
|
62
|
+
job = event.payload[:job]
|
|
63
|
+
|
|
64
|
+
Eyeloupe::Job.where(job_id: job.job_id).update(status: :discarded)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param [ActiveJob::Base] job The job object
|
|
68
|
+
# @return [Array, nil]
|
|
69
|
+
def args_info(job)
|
|
70
|
+
if job.class.log_arguments? && job.arguments.any?
|
|
71
|
+
job.arguments
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# @param [ActiveSupport::Notifications::Event] event The event object
|
|
78
|
+
# @return [String] The name of the queue
|
|
79
|
+
def queue_name(event)
|
|
80
|
+
event.payload[:job].queue_name
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param [ActiveSupport::Notifications::Event] event The event object
|
|
84
|
+
# @return [String] The name of the adapter
|
|
85
|
+
def adapter_name(event)
|
|
86
|
+
event.payload[:adapter].class.name.demodulize.remove("Adapter")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param [ActiveSupport::Notifications::Event] event The event object
|
|
90
|
+
# @return [Time, nil] The time the job was scheduled at
|
|
91
|
+
def scheduled_at(event)
|
|
92
|
+
return unless event.payload[:job].scheduled_at
|
|
93
|
+
Time.at(event.payload[:job].scheduled_at).utc
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @param [String] event The event to subscribe to
|
|
97
|
+
# @param [Proc] block The block to execute when the event is triggered
|
|
98
|
+
# @yield [ActiveSupport::Notifications::Event] The event object
|
|
99
|
+
def subscribe(event, &block)
|
|
100
|
+
@subs << ActiveSupport::Notifications.subscribe(event) do |*args|
|
|
101
|
+
block.call(ActiveSupport::Notifications::Event.new(*args))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def unsubscribe
|
|
106
|
+
@subs.each do |sub|
|
|
107
|
+
ActiveSupport::Notifications.unsubscribe(sub)
|
|
108
|
+
end
|
|
109
|
+
@subs = []
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/eyeloupe/version.rb
CHANGED
data/lib/eyeloupe.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eyeloupe
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexandre Lion
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-10-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sprockets-rails
|
|
@@ -80,6 +80,20 @@ dependencies:
|
|
|
80
80
|
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: 4.1.0
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: nokogiri
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 1.15.4
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 1.15.4
|
|
83
97
|
- !ruby/object:Gem::Dependency
|
|
84
98
|
name: sqlite3
|
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -154,13 +168,17 @@ files:
|
|
|
154
168
|
- app/controllers/eyeloupe/data_controller.rb
|
|
155
169
|
- app/controllers/eyeloupe/exceptions_controller.rb
|
|
156
170
|
- app/controllers/eyeloupe/in_requests_controller.rb
|
|
171
|
+
- app/controllers/eyeloupe/jobs_controller.rb
|
|
157
172
|
- app/controllers/eyeloupe/out_requests_controller.rb
|
|
158
173
|
- app/helpers/eyeloupe/application_helper.rb
|
|
174
|
+
- app/helpers/eyeloupe/jobs_helper.rb
|
|
175
|
+
- app/helpers/eyeloupe/request_helper.rb
|
|
159
176
|
- app/jobs/eyeloupe/application_job.rb
|
|
160
177
|
- app/mailers/eyeloupe/application_mailer.rb
|
|
161
178
|
- app/models/eyeloupe/application_record.rb
|
|
162
179
|
- app/models/eyeloupe/exception.rb
|
|
163
180
|
- app/models/eyeloupe/in_request.rb
|
|
181
|
+
- app/models/eyeloupe/job.rb
|
|
164
182
|
- app/models/eyeloupe/out_request.rb
|
|
165
183
|
- app/views/eyeloupe/exceptions/_frame.html.erb
|
|
166
184
|
- app/views/eyeloupe/exceptions/index.html.erb
|
|
@@ -168,9 +186,13 @@ files:
|
|
|
168
186
|
- app/views/eyeloupe/in_requests/_frame.html.erb
|
|
169
187
|
- app/views/eyeloupe/in_requests/index.html.erb
|
|
170
188
|
- app/views/eyeloupe/in_requests/show.html.erb
|
|
189
|
+
- app/views/eyeloupe/jobs/_frame.html.erb
|
|
190
|
+
- app/views/eyeloupe/jobs/index.html.erb
|
|
191
|
+
- app/views/eyeloupe/jobs/show.html.erb
|
|
171
192
|
- app/views/eyeloupe/out_requests/_frame.html.erb
|
|
172
193
|
- app/views/eyeloupe/out_requests/index.html.erb
|
|
173
194
|
- app/views/eyeloupe/out_requests/show.html.erb
|
|
195
|
+
- app/views/eyeloupe/shared/_job_status.html.erb
|
|
174
196
|
- app/views/eyeloupe/shared/_status_code.html.erb
|
|
175
197
|
- app/views/eyeloupe/shared/_verb.html.erb
|
|
176
198
|
- app/views/layouts/eyeloupe/application.html.erb
|
|
@@ -180,12 +202,14 @@ files:
|
|
|
180
202
|
- db/migrate/20230518175305_create_eyeloupe_in_requests.rb
|
|
181
203
|
- db/migrate/20230525125352_create_eyeloupe_out_requests.rb
|
|
182
204
|
- db/migrate/20230604190442_create_eyeloupe_exceptions.rb
|
|
205
|
+
- db/migrate/20230827161224_create_eyeloupe_jobs.rb
|
|
183
206
|
- lib/eyeloupe.rb
|
|
184
207
|
- lib/eyeloupe/concerns/rescuable.rb
|
|
185
208
|
- lib/eyeloupe/configuration.rb
|
|
186
209
|
- lib/eyeloupe/engine.rb
|
|
187
210
|
- lib/eyeloupe/processors/exception.rb
|
|
188
211
|
- lib/eyeloupe/processors/in_request.rb
|
|
212
|
+
- lib/eyeloupe/processors/job.rb
|
|
189
213
|
- lib/eyeloupe/processors/out_request.rb
|
|
190
214
|
- lib/eyeloupe/request_middleware.rb
|
|
191
215
|
- lib/eyeloupe/version.rb
|