eyeloupe 0.3.0 → 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 +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://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
|