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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a2fc3039c5a9cdcbd79a0a5639b74c52c3fc42f0c96fc569f92ce38255e7768
4
- data.tar.gz: 4de09b66849266fa6e43af59a55a60ab60464bb1310f8dff5a28a8e84904dac9
3
+ metadata.gz: bff4826ab05209a18e73946c70c010ba1c6196514c639fc3066820ac483602ba
4
+ data.tar.gz: c6f770934433bd5f6592021227a3caf1214a6f0a7e828141b1955c6e9eb7121f
5
5
  SHA512:
6
- metadata.gz: 0c02695f8f4f26d09df3fda942691bf6729c9c1774b95acc1e9663e48c0ae25bad3d6991804a94163ff6595c8dbc8e56cb7f6dbd6f0be6eb195a2332446b5d9d
7
- data.tar.gz: b61c44badf4b0713186338a76d0a246c1f87586d88e8404861931b326aafcfc8945977222e2296fc3d10aee2eb2f369df99cdcc84d4a8c9bb508453f579a6927
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 (beta)</h3>
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__-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/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
- @query = params[:q].present? ? model.where('path LIKE ?', "%#{params[:q].strip}%").order(created_at: :desc)
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,4 @@
1
+ module Eyeloupe
2
+ module JobsHelper
3
+ end
4
+ 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
@@ -0,0 +1,7 @@
1
+ module Eyeloupe
2
+ class Job < ApplicationRecord
3
+ validates :job_id, uniqueness: true
4
+
5
+ enum status: [:enqueued, :running, :completed, :failed, :discarded]
6
+ end
7
+ 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.payload %></pre>
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"><%= @request.format == "application/json" ? JSON.pretty_generate(JSON.parse(@request.response || "{}")) : @request.response %></pre>
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.payload %></pre>
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"><%= @request.format == "application/json" ? JSON.pretty_generate(JSON.parse(@request.response || "{}")) : @request.response %></pre>
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
@@ -5,6 +5,9 @@ module Eyeloupe
5
5
  class Configuration
6
6
  include Singleton
7
7
 
8
+ # @return [Symbol|Nil]
9
+ attr_accessor :database
10
+
8
11
  # @return [Array<String>]
9
12
  attr_accessor :excluded_paths
10
13
 
@@ -24,4 +27,4 @@ module Eyeloupe
24
27
  end
25
28
  end
26
29
 
27
- end
30
+ end
@@ -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
@@ -1,4 +1,4 @@
1
1
  module Eyeloupe
2
2
  # @return [String]
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.0"
4
4
  end
data/lib/eyeloupe.rb CHANGED
@@ -6,6 +6,7 @@ require 'eyeloupe/configuration'
6
6
  require 'eyeloupe/processors/in_request'
7
7
  require 'eyeloupe/processors/out_request'
8
8
  require 'eyeloupe/processors/exception'
9
+ require 'eyeloupe/processors/job'
9
10
  require 'eyeloupe/concerns/rescuable'
10
11
 
11
12
  require 'pagy'
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.3.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-06-17 00:00:00.000000000 Z
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