eyeloupe 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fdc0f9276e2c518c9b396f1acbc856460fb3eabdf926cbd8940bc6f33a9ec19c
4
- data.tar.gz: c3fa4ae7197c7de0bcbb3790bf0b4b1eb2bd93a6bee3ecca5030b4960b5c74ce
3
+ metadata.gz: bff4826ab05209a18e73946c70c010ba1c6196514c639fc3066820ac483602ba
4
+ data.tar.gz: c6f770934433bd5f6592021227a3caf1214a6f0a7e828141b1955c6e9eb7121f
5
5
  SHA512:
6
- metadata.gz: c7bf1df1813f5b9791ef17c39149179b7450ba7009d61950edd1bde80edad27be1fb1ea043516dd1c51637040874e05a096c5e9a5fbe5f22379c801e9a1c8943
7
- data.tar.gz: e2e24fd0d42f2e430eea07f8395e4a3a2aa6ea0ffd02a44e290339ccc1feaf92fab07af6e2e175ac7cae4a0b95ba13c2c992ecef0d9d4f55cae96085c6cca995
6
+ metadata.gz: ad01737f9bd77b93a765fccbe76e44738d87975f8e9f0abef12942330bb3b655e45872c894c360b0cf454d334a3b283cfec97c7eadd2f4f4fb8fab44eab98cc5
7
+ data.tar.gz: bbee31e3b76dfd55e642b8577780e78058ecdf47ef2bf09acc2c216687d0c93fb3c2a4a513171afd0877a1f04a363bfa4869db27d82894a7a879f99fc202c23e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.4.0
2
+
3
+ - Add support for ActiveJob
4
+ - Fix pretty formatting of payloads
5
+
1
6
  ## 0.3.1
2
7
 
3
8
  - Add optional database config (thanks to @kiskoza).
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.
@@ -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://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_)
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
- @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
@@ -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
@@ -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.1"
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.1
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-07-02 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