eyeloupe 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -1
- data/README.md +23 -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/application_record.rb +4 -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/configuration.rb +4 -1
- 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
@@ -1,3 +1,12 @@
|
|
1
|
+
## 0.4.0
|
2
|
+
|
3
|
+
- Add support for ActiveJob
|
4
|
+
- Fix pretty formatting of payloads
|
5
|
+
|
6
|
+
## 0.3.1
|
7
|
+
|
8
|
+
- Add optional database config (thanks to @kiskoza).
|
9
|
+
|
1
10
|
## 0.3.0
|
2
11
|
|
3
12
|
- Add exceptions: Framework + ActiveJob + Sidekiq Worker exceptions.
|
@@ -10,4 +19,4 @@
|
|
10
19
|
|
11
20
|
## 0.1.0
|
12
21
|
|
13
|
-
- Initial release including incoming and outgoing requests.
|
22
|
+
- Initial release including incoming and outgoing requests.
|
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.
|
@@ -64,6 +64,7 @@ Eyeloupe.configure do |config|
|
|
64
64
|
config.capture = Rails.env.development?
|
65
65
|
config.openai_access_key = "your-openai-access-key"
|
66
66
|
config.openai_model = "gpt-4"
|
67
|
+
config.database = 'eyeloupe'
|
67
68
|
end
|
68
69
|
```
|
69
70
|
|
@@ -71,6 +72,26 @@ end
|
|
71
72
|
- `capture` is a boolean to enable/disable Eyeloupe capture. By default, it's set to `true`.
|
72
73
|
- `openai_access_key` is the access key to use the OpenAI API. You can get one [here](https://platform.openai.com/).
|
73
74
|
- `openai_model` is the model to use for the OpenAI API. You can find the list of available models [here](https://platform.openai.com/docs/models).
|
75
|
+
- `database` is an optional database config Eyeloupe will use ([Database](#database)).
|
76
|
+
|
77
|
+
|
78
|
+
### Database
|
79
|
+
|
80
|
+
By default, Eyeloupe uses the same database as your application. If you want to use a different database to keep your production environment clean, you can add a new database config in your `config/database.yml` file:
|
81
|
+
|
82
|
+
```yml
|
83
|
+
development:
|
84
|
+
primary:
|
85
|
+
<<: *default
|
86
|
+
database: db/development.sqlite3
|
87
|
+
eyeloupe:
|
88
|
+
<<: *default
|
89
|
+
database: db/eyeloupe.sqlite3
|
90
|
+
migrations_paths: <%= Gem.loaded_specs['eyeloupe'].full_gem_path + '/db/migrate' %>
|
91
|
+
schema_dump: false
|
92
|
+
```
|
93
|
+
|
94
|
+
Using this you can skip the `eyeloupe:install:migrations` step, but do not forget to run `rails db:migrate RAILS_ENV=eyeloupe` to setup the database.
|
74
95
|
|
75
96
|
### Exception handling
|
76
97
|
|
@@ -120,13 +141,6 @@ Eyeloupe is not a performance-oriented tool, the request time is the same you ca
|
|
120
141
|
|
121
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.
|
122
143
|
|
123
|
-
## Roadmap
|
124
|
-
|
125
|
-
- [x] Exceptions - Track all the exceptions thrown by your application
|
126
|
-
- [x] AI assistant - Use OpenAI API to help you to solve your exceptions
|
127
|
-
- [ ] Custom links to the menu - To access all of your debug tool in one place (Sidekiq web, Mailhog, etc.)
|
128
|
-
- [ ] Refactoring / clean code - To make the code more readable and maintainable
|
129
|
-
|
130
144
|
## Contributing
|
131
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**.
|
132
146
|
|
@@ -144,7 +158,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
144
158
|
|
145
159
|
## Contact
|
146
160
|
|
147
|
-
[![](https://img.shields.io/badge/@alxlion__-
|
161
|
+
[![](https://img.shields.io/badge/@alxlion__-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/alxlion_)
|
148
162
|
|
149
163
|
Project Link: [https://github.com/alxlion/eyeloupe](https://github.com/alxlion/eyeloupe)
|
150
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
|
@@ -1,5 +1,9 @@
|
|
1
1
|
module Eyeloupe
|
2
2
|
class ApplicationRecord < ActiveRecord::Base
|
3
3
|
self.abstract_class = true
|
4
|
+
|
5
|
+
if Eyeloupe.configuration.database
|
6
|
+
connects_to database: { writing: Eyeloupe.configuration.database.to_sym, reading: Eyeloupe.configuration.database.to_sym }
|
7
|
+
end
|
4
8
|
end
|
5
9
|
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
|