eyeloupe 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +129 -0
- data/Rakefile +8 -0
- data/app/assets/builds/eyeloupe.css +1 -0
- data/app/assets/config/eyeloupe_manifest.js +3 -0
- data/app/assets/images/eyeloupe/logo.png +0 -0
- data/app/assets/javascripts/eyeloupe/application.js +5 -0
- data/app/assets/javascripts/eyeloupe/controllers/application.js +9 -0
- data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/nav_controller.js +13 -0
- data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/pause_controller.js +29 -0
- data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/refresh_controller.js +53 -0
- data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/search_controller.js +11 -0
- data/app/assets/javascripts/eyeloupe/controllers/index.js +11 -0
- data/app/assets/stylesheets/application.tailwind.css +42 -0
- data/app/assets/stylesheets/eyeloupe/application.css +16 -0
- data/app/controllers/concerns/eyeloupe/searchable.rb +20 -0
- data/app/controllers/eyeloupe/application_controller.rb +17 -0
- data/app/controllers/eyeloupe/configs_controller.rb +20 -0
- data/app/controllers/eyeloupe/data_controller.rb +15 -0
- data/app/controllers/eyeloupe/in_requests_controller.rb +24 -0
- data/app/controllers/eyeloupe/out_requests_controller.rb +27 -0
- data/app/helpers/eyeloupe/application_helper.rb +5 -0
- data/app/jobs/eyeloupe/application_job.rb +4 -0
- data/app/mailers/eyeloupe/application_mailer.rb +6 -0
- data/app/models/eyeloupe/application_record.rb +5 -0
- data/app/models/eyeloupe/in_request.rb +4 -0
- data/app/models/eyeloupe/out_request.rb +4 -0
- data/app/views/eyeloupe/in_requests/_frame.html.erb +44 -0
- data/app/views/eyeloupe/in_requests/index.html.erb +18 -0
- data/app/views/eyeloupe/in_requests/show.html.erb +82 -0
- data/app/views/eyeloupe/out_requests/_frame.html.erb +46 -0
- data/app/views/eyeloupe/out_requests/index.html.erb +18 -0
- data/app/views/eyeloupe/out_requests/show.html.erb +69 -0
- data/app/views/eyeloupe/shared/_status_code.html.erb +21 -0
- data/app/views/eyeloupe/shared/_verb.html.erb +17 -0
- data/app/views/layouts/eyeloupe/application.html.erb +203 -0
- data/config/importmap.rb +4 -0
- data/config/routes.rb +12 -0
- data/config/tailwind.config.js +22 -0
- data/db/migrate/20230518175305_create_eyeloupe_in_requests.rb +22 -0
- data/db/migrate/20230525125352_create_eyeloupe_out_requests.rb +18 -0
- data/lib/eyeloupe/configuration.rb +20 -0
- data/lib/eyeloupe/engine.rb +21 -0
- data/lib/eyeloupe/http.rb +19 -0
- data/lib/eyeloupe/processors/in_request.rb +148 -0
- data/lib/eyeloupe/processors/out_request.rb +76 -0
- data/lib/eyeloupe/request_middleware.rb +72 -0
- data/lib/eyeloupe/version.rb +4 -0
- data/lib/eyeloupe.rb +21 -0
- data/lib/tasks/eyeloupe_tasks.rake +9 -0
- metadata +195 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
<div data-controller="eyeloupe--search">
|
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">Requests</h1>
|
5
|
+
<p class="mt-2 text-sm text-gray-700">All incoming request to 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 path" 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: in_requests_path(frame: true), data: {"eyeloupe--refresh-target": "frame", "eyeloupe--search-target": "frame"} do %><% end %>
|
17
|
+
</div>
|
18
|
+
</div>
|
@@ -0,0 +1,82 @@
|
|
1
|
+
<div>
|
2
|
+
<div class="px-4 sm:px-0">
|
3
|
+
<h3 class="text-xl font-semibold leading-7 text-gray-900">Request 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">Time</dt>
|
9
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
10
|
+
<%= @request.created_at.to_formatted_s(:long) %> (<%= distance_of_time_in_words(@request.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">Hostname</dt>
|
15
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.hostname %></dd>
|
16
|
+
</div>
|
17
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
18
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Method</dt>
|
19
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
20
|
+
<%= render "eyeloupe/shared/verb", verb: @request.verb %>
|
21
|
+
</dd>
|
22
|
+
</div>
|
23
|
+
<% if @request.controller.present? %>
|
24
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
25
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Controller</dt>
|
26
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.controller %></dd>
|
27
|
+
</div>
|
28
|
+
<% end %>
|
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">Path</dt>
|
31
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.path %></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">Status</dt>
|
35
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
36
|
+
<%= render "eyeloupe/shared/status_code", code: @request.status %>
|
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">Duration</dt>
|
41
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
42
|
+
<%= @request.duration %> ms
|
43
|
+
<% if @request.db_duration.present? && @request.view_duration.present? %>
|
44
|
+
(ActiveRecord: <%= @request.db_duration %> ms, Views: <%= @request.view_duration %> ms)
|
45
|
+
<% end %>
|
46
|
+
</dd>
|
47
|
+
</div>
|
48
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
49
|
+
<dt class="text-base font-medium leading-6 text-gray-900">IP Address</dt>
|
50
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.ip %></dd>
|
51
|
+
</div>
|
52
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
53
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Payload</dt>
|
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
|
+
<% if @request.payload.present? %>
|
56
|
+
<pre class="p-2"><%= @request.payload %></pre>
|
57
|
+
<% else %>
|
58
|
+
<p class="text-gray-400 p-2">No payload</p>
|
59
|
+
<% end %>
|
60
|
+
</dd>
|
61
|
+
</div>
|
62
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
63
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Headers</dt>
|
64
|
+
<dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
|
65
|
+
<pre class="p-2"><%= JSON.pretty_generate(JSON.parse(@request.headers || "{}")) %></pre>
|
66
|
+
</dd>
|
67
|
+
</div>
|
68
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
69
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Session</dt>
|
70
|
+
<dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
|
71
|
+
<pre class="p-2" ><%= JSON.pretty_generate(JSON.parse(@request.session || "{}")) %></pre>
|
72
|
+
</dd>
|
73
|
+
</div>
|
74
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
75
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Response</dt>
|
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>
|
78
|
+
</dd>
|
79
|
+
</div>
|
80
|
+
</dl>
|
81
|
+
</div>
|
82
|
+
</div>
|
@@ -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">Verb</th>
|
9
|
+
<th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Host</th>
|
10
|
+
<th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Path</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">Duration</th>
|
13
|
+
<th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Occurred</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
|
+
<% @requests.each do |request| %>
|
21
|
+
<tr>
|
22
|
+
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-base font-medium text-gray-900 sm:pl-0">
|
23
|
+
<%= render "eyeloupe/shared/verb", verb: request.verb %>
|
24
|
+
</td>
|
25
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.hostname %></td>
|
26
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.path.truncate(50) %></td>
|
27
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500">
|
28
|
+
<%= render "eyeloupe/shared/status_code", code: request.status %>
|
29
|
+
</td>
|
30
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.duration %> ms</td>
|
31
|
+
<td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= distance_of_time_in_words(request.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", out_request_path(request), 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">
|
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">HTTP Client</h1>
|
5
|
+
<p class="mt-2 text-sm text-gray-700">All outbound HTTP requests made by 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 path" 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: out_requests_path(frame: true), data: {"eyeloupe--refresh-target": "frame", "eyeloupe--search-target": "frame"} do %><% end %>
|
17
|
+
</div>
|
18
|
+
</div>
|
@@ -0,0 +1,69 @@
|
|
1
|
+
<div>
|
2
|
+
<div class="px-4 sm:px-0">
|
3
|
+
<h3 class="text-xl font-semibold leading-7 text-gray-900">HTTP Client Request 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">Time</dt>
|
9
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
10
|
+
<%= @request.created_at.to_formatted_s(:long) %> (<%= distance_of_time_in_words(@request.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">Hostname</dt>
|
15
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.hostname %></dd>
|
16
|
+
</div>
|
17
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
18
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Method</dt>
|
19
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
20
|
+
<%= render "eyeloupe/shared/verb", verb: @request.verb %>
|
21
|
+
</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">Path</dt>
|
25
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.path %></dd>
|
26
|
+
</div>
|
27
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
28
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Status</dt>
|
29
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
30
|
+
<%= render "eyeloupe/shared/status_code", code: @request.status %>
|
31
|
+
</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">Duration</dt>
|
35
|
+
<dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
36
|
+
<%= @request.duration %> ms
|
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">Payload</dt>
|
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
|
+
<% if @request.payload.present? %>
|
43
|
+
<pre class="p-2"><%= @request.payload %></pre>
|
44
|
+
<% else %>
|
45
|
+
<p class="text-gray-400 p-2">No payload</p>
|
46
|
+
<% end %>
|
47
|
+
</dd>
|
48
|
+
</div>
|
49
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
50
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Request headers</dt>
|
51
|
+
<dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
|
52
|
+
<pre class="p-2"><%= JSON.pretty_generate(JSON.parse(@request.req_headers || "{}")) %></pre>
|
53
|
+
</dd>
|
54
|
+
</div>
|
55
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
56
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Response headers</dt>
|
57
|
+
<dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
|
58
|
+
<pre class="p-2"><%= JSON.pretty_generate(JSON.parse(@request.res_headers || "{}")) %></pre>
|
59
|
+
</dd>
|
60
|
+
</div>
|
61
|
+
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
62
|
+
<dt class="text-base font-medium leading-6 text-gray-900">Response</dt>
|
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>
|
65
|
+
</dd>
|
66
|
+
</div>
|
67
|
+
</dl>
|
68
|
+
</div>
|
69
|
+
</div>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<% if code.to_s[0] == "2" %>
|
2
|
+
<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">
|
3
|
+
<%= code %>
|
4
|
+
</span>
|
5
|
+
<% elsif code.to_s[0] == "3" %>
|
6
|
+
<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">
|
7
|
+
<%= code %>
|
8
|
+
</span>
|
9
|
+
<% elsif code.to_s[0] == "4" %>
|
10
|
+
<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">
|
11
|
+
<%= code %>
|
12
|
+
</span>
|
13
|
+
<% elsif code.to_s[0] == "5" %>
|
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
|
+
<%= code %>
|
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
|
+
<%= code %>
|
20
|
+
</span>
|
21
|
+
<% end %>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<% if verb.downcase == "post" || verb.downcase == "patch" || verb.downcase == "put" %>
|
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
|
+
<%= verb %>
|
4
|
+
</span>
|
5
|
+
<% elsif verb.downcase == "get" || verb.downcase == "options" %>
|
6
|
+
<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">
|
7
|
+
<%= verb %>
|
8
|
+
</span>
|
9
|
+
<% elsif verb.downcase == "delete" %>
|
10
|
+
<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">
|
11
|
+
<%= verb %>
|
12
|
+
</span>
|
13
|
+
<% else %>
|
14
|
+
<span class="inline-flex items-center rounded-md bg-purple-50 px-2 py-1 text-base font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10">
|
15
|
+
<%= verb %>
|
16
|
+
</span>
|
17
|
+
<% end %>
|
@@ -0,0 +1,203 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Eyeloupe</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
<%= javascript_importmap_tags "eyeloupe/application" %>
|
8
|
+
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
12
|
+
|
13
|
+
<%= stylesheet_link_tag "eyeloupe", media: "all" %>
|
14
|
+
</head>
|
15
|
+
<body class="bg-gray-50">
|
16
|
+
|
17
|
+
<div data-controller="eyeloupe--nav">
|
18
|
+
<div data-controller="eyeloupe--refresh">
|
19
|
+
<!-- Off-canvas menu for mobile, show/hide based on off-canvas menu state. -->
|
20
|
+
<div class="relative z-50 lg:hidden hidden" role="dialog" aria-modal="true" data-eyeloupe--nav-target="content">
|
21
|
+
<div class="fixed inset-0 bg-gray-900/80"></div>
|
22
|
+
|
23
|
+
<div class="fixed inset-0 flex">
|
24
|
+
<div class="relative mr-16 flex w-full max-w-xs flex-1">
|
25
|
+
<div class="absolute left-full top-0 flex w-16 justify-center pt-5">
|
26
|
+
<button data-action="eyeloupe--nav#close" type="button" class="-m-2.5 p-2.5">
|
27
|
+
<span class="sr-only">Close sidebar</span>
|
28
|
+
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
29
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
30
|
+
</svg>
|
31
|
+
</button>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
|
35
|
+
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-50 px-6 pb-2">
|
36
|
+
<%= link_to root_url, class:"flex h-16 shrink-0 items-center" do %>
|
37
|
+
<%= image_tag "eyeloupe/logo.png", class: "h-7 w-auto" %>
|
38
|
+
<h1 class="ml-2 text-xl font-semibold text-gray-700">Eyeloupe</h1>
|
39
|
+
<% end %>
|
40
|
+
<nav class="flex flex-1 flex-col">
|
41
|
+
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
42
|
+
<li>
|
43
|
+
<ul role="list" class="-mx-2 space-y-1">
|
44
|
+
<li>
|
45
|
+
<%= link_to in_requests_path, class:"#{request.path.include?('/in_requests') ? '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 %>
|
46
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
47
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
48
|
+
<path d="M21 17l-18 0"></path>
|
49
|
+
<path d="M6 10l-3 -3l3 -3"></path>
|
50
|
+
<path d="M3 7l18 0"></path>
|
51
|
+
<path d="M18 20l3 -3l-3 -3"></path>
|
52
|
+
</svg>
|
53
|
+
Requests
|
54
|
+
<% end %>
|
55
|
+
</li>
|
56
|
+
<li>
|
57
|
+
<%= link_to out_requests_path, class:"#{request.path.include?('/out_requests') ? '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 %>
|
58
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
59
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
60
|
+
<path d="M21 12a9 9 0 1 0 -9 9"></path>
|
61
|
+
<path d="M3.6 9h16.8"></path>
|
62
|
+
<path d="M3.6 15h8.4"></path>
|
63
|
+
<path d="M11.578 3a17 17 0 0 0 0 18"></path>
|
64
|
+
<path d="M12.5 3c1.719 2.755 2.5 5.876 2.5 9"></path>
|
65
|
+
<path d="M18 21v-7m3 3l-3 -3l-3 3"></path>
|
66
|
+
</svg>
|
67
|
+
HTTP Client
|
68
|
+
<% end %>
|
69
|
+
</li>
|
70
|
+
</ul>
|
71
|
+
</li>
|
72
|
+
</ul>
|
73
|
+
</nav>
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
|
79
|
+
<!-- Static sidebar for desktop -->
|
80
|
+
<div class="mx-auto">
|
81
|
+
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
|
82
|
+
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-50 px-6">
|
83
|
+
<%= link_to root_path, class:"flex h-16 shrink-0 items-center" do %>
|
84
|
+
<%= image_tag "eyeloupe/logo.png", class: "h-12 w-auto" %>
|
85
|
+
<h1 class="ml-2 text-2xl font-semibold text-gray-700">Eyeloupe</h1>
|
86
|
+
<% end %>
|
87
|
+
<nav class="flex flex-1 flex-col">
|
88
|
+
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
89
|
+
<li>
|
90
|
+
<ul role="list" class="-mx-2 space-y-1">
|
91
|
+
<li>
|
92
|
+
<%= link_to in_requests_path, class:"#{request.path.include?('/in_requests') ? '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 %>
|
93
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
94
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
95
|
+
<path d="M21 17l-18 0"></path>
|
96
|
+
<path d="M6 10l-3 -3l3 -3"></path>
|
97
|
+
<path d="M3 7l18 0"></path>
|
98
|
+
<path d="M18 20l3 -3l-3 -3"></path>
|
99
|
+
</svg>
|
100
|
+
Requests
|
101
|
+
<% end %>
|
102
|
+
</li>
|
103
|
+
<li>
|
104
|
+
<%= link_to out_requests_path, class:"#{request.path.include?('/out_requests') ? '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 %>
|
105
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
106
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
107
|
+
<path d="M21 12a9 9 0 1 0 -9 9"></path>
|
108
|
+
<path d="M3.6 9h16.8"></path>
|
109
|
+
<path d="M3.6 15h8.4"></path>
|
110
|
+
<path d="M11.578 3a17 17 0 0 0 0 18"></path>
|
111
|
+
<path d="M12.5 3c1.719 2.755 2.5 5.876 2.5 9"></path>
|
112
|
+
<path d="M18 21v-7m3 3l-3 -3l-3 3"></path>
|
113
|
+
</svg>
|
114
|
+
HTTP Client
|
115
|
+
<% end %>
|
116
|
+
</li>
|
117
|
+
</ul>
|
118
|
+
</li>
|
119
|
+
</ul>
|
120
|
+
</nav>
|
121
|
+
</div>
|
122
|
+
</div>
|
123
|
+
</div>
|
124
|
+
|
125
|
+
<div class="hidden lg:flex py-4 justify-end items-end px-4 gap-x-2">
|
126
|
+
<%= link_to configs_path(value: "#{!@eyeloupe_capture}"), title: "Pause data collection", data: { "turbo_method": "put" }, class: "#{@eyeloupe_capture ? "text-gray-500 hover:bg-gray-300 bg-gray-200" : "bg-red-500 text-white hover:bg-red-600"} rounded-md p-1 transition duration-500 inline-block hidden" do %>
|
127
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
128
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
129
|
+
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
|
130
|
+
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
|
131
|
+
</svg>
|
132
|
+
<% end %>
|
133
|
+
<button title="Auto refresh data" data-action="eyeloupe--refresh#toggle" data-eyeloupe--refresh-target="btn" class="rounded-md text-gray-500 hover:bg-gray-300 bg-gray-200 p-1 transition duration-500 inline-block">
|
134
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
135
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
136
|
+
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
137
|
+
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
138
|
+
</svg>
|
139
|
+
</button>
|
140
|
+
<%= link_to data_path, title: "Delete all Eyeloupe data", data: { "turbo_method": "delete", "turbo_confirm": "Are you sure to delete all Eyeloupe data?" }, class: "rounded-md text-gray-500 hover:bg-gray-300 bg-gray-200 p-1 transition duration-500 inline-block" do %>
|
141
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
142
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
143
|
+
<path d="M4 7l16 0"></path>
|
144
|
+
<path d="M10 11l0 6"></path>
|
145
|
+
<path d="M14 11l0 6"></path>
|
146
|
+
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path>
|
147
|
+
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path>
|
148
|
+
</svg>
|
149
|
+
<% end %>
|
150
|
+
</div>
|
151
|
+
|
152
|
+
<div class="sticky top-0 z-40 flex justify-between px-4 gap-x-6 bg-white py-4 shadow-sm sm:px-6 lg:hidden">
|
153
|
+
<div class="flex items-center gap-x-6">
|
154
|
+
<button data-action="eyeloupe--nav#open" type="button" class="-m-2.5 p-2.5 text-red-500 lg:hidden">
|
155
|
+
<span class="sr-only">Open sidebar</span>
|
156
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
157
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
158
|
+
</svg>
|
159
|
+
</button>
|
160
|
+
<%= link_to root_url, class: "flex gap-x-1" do %>
|
161
|
+
<%= image_tag "eyeloupe/logo.png", class: "h-7 w-auto" %>
|
162
|
+
<h1 class="ml-2 text-xl font-semibold text-gray-700">Eyeloupe</h1>
|
163
|
+
<% end %>
|
164
|
+
</div>
|
165
|
+
<div class="py-4 flex justify-end items-end px-4 gap-x-2">
|
166
|
+
<%= link_to configs_path(value: "#{!@eyeloupe_capture}"), title: "Pause data collection", data: { "turbo_method": "put" }, class: "#{@eyeloupe_capture ? "text-gray-500 hover:bg-gray-300 bg-gray-200" : "bg-red-500 text-white hover:bg-red-600"} rounded-md p-1 transition duration-500 inline-block hidden" do %>
|
167
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
168
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
169
|
+
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
|
170
|
+
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
|
171
|
+
</svg>
|
172
|
+
<% end %>
|
173
|
+
<button title="Auto refresh data" data-action="eyeloupe--refresh#toggle" data-eyeloupe--refresh-target="btn" class="rounded-md text-gray-500 hover:bg-gray-300 bg-gray-200 p-1 transition duration-500 inline-block">
|
174
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
175
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
176
|
+
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
177
|
+
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
178
|
+
</svg>
|
179
|
+
</button>
|
180
|
+
<%= link_to data_path, title: "Delete all Eyeloupe data", data: { "turbo_method": "delete", "turbo_confirm": "Are you sure to delete all Eyeloupe data?" }, class: "rounded-md text-gray-500 hover:bg-gray-300 bg-gray-200 p-1 transition duration-500 inline-block" do %>
|
181
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
182
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
183
|
+
<path d="M4 7l16 0"></path>
|
184
|
+
<path d="M10 11l0 6"></path>
|
185
|
+
<path d="M14 11l0 6"></path>
|
186
|
+
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path>
|
187
|
+
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path>
|
188
|
+
</svg>
|
189
|
+
<% end %>
|
190
|
+
</div>
|
191
|
+
</div>
|
192
|
+
|
193
|
+
<main class="py-10 lg:pl-72 px-5 mx-auto">
|
194
|
+
|
195
|
+
<div class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
|
196
|
+
<%= yield %>
|
197
|
+
</div>
|
198
|
+
</main>
|
199
|
+
</div>
|
200
|
+
</div>
|
201
|
+
|
202
|
+
</body>
|
203
|
+
</html>
|
data/config/importmap.rb
ADDED
@@ -0,0 +1,4 @@
|
|
1
|
+
pin_all_from File.expand_path("../app/assets/javascripts", __dir__)
|
2
|
+
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
|
3
|
+
pin "@hotwired/stimulus", to: "https://ga.jspm.io/npm:@hotwired/stimulus@3.0.1/dist/stimulus.js"
|
4
|
+
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
|
data/config/routes.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
const defaultTheme = require('tailwindcss/defaultTheme')
|
2
|
+
|
3
|
+
module.exports = {
|
4
|
+
content: [
|
5
|
+
'./public/*.html',
|
6
|
+
'./app/helpers/**/*.rb',
|
7
|
+
'./app/assets/javascripts/**/*.js',
|
8
|
+
'./app/views/**/*.{erb,haml,html,slim}'
|
9
|
+
],
|
10
|
+
theme: {
|
11
|
+
extend: {
|
12
|
+
fontFamily: {
|
13
|
+
sans: ['Fira Sans', 'sans-serif', ...defaultTheme.fontFamily.sans],
|
14
|
+
},
|
15
|
+
},
|
16
|
+
},
|
17
|
+
plugins: [
|
18
|
+
require('@tailwindcss/forms'),
|
19
|
+
require('@tailwindcss/aspect-ratio'),
|
20
|
+
require('@tailwindcss/typography'),
|
21
|
+
]
|
22
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class CreateEyeloupeInRequests < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :eyeloupe_in_requests do |t|
|
4
|
+
t.string :verb
|
5
|
+
t.string :hostname
|
6
|
+
t.string :controller
|
7
|
+
t.string :path
|
8
|
+
t.string :format
|
9
|
+
t.integer :status
|
10
|
+
t.integer :duration
|
11
|
+
t.integer :db_duration
|
12
|
+
t.integer :view_duration
|
13
|
+
t.string :ip
|
14
|
+
t.text :payload
|
15
|
+
t.text :headers
|
16
|
+
t.text :session
|
17
|
+
t.text :response
|
18
|
+
|
19
|
+
t.timestamps
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateEyeloupeOutRequests < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :eyeloupe_out_requests do |t|
|
4
|
+
t.string :verb
|
5
|
+
t.string :hostname
|
6
|
+
t.string :path
|
7
|
+
t.string :format
|
8
|
+
t.integer :status
|
9
|
+
t.integer :duration
|
10
|
+
t.text :payload
|
11
|
+
t.text :req_headers
|
12
|
+
t.text :res_headers
|
13
|
+
t.text :response
|
14
|
+
|
15
|
+
t.timestamps
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module Eyeloupe
|
5
|
+
class Configuration
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
# @return [Array<String>]
|
9
|
+
attr_accessor :excluded_paths
|
10
|
+
|
11
|
+
# @return [Boolean]
|
12
|
+
attr_accessor :capture
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@excluded_paths = %w[]
|
16
|
+
@capture = true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'importmap-rails'
|
2
|
+
|
3
|
+
module Eyeloupe
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace Eyeloupe
|
6
|
+
|
7
|
+
initializer "eyeloupe.assets" do |app|
|
8
|
+
app.config.assets.precompile += %w[ eyeloupe_manifest ]
|
9
|
+
end
|
10
|
+
|
11
|
+
initializer 'eyeloupe.add_middleware' do |app|
|
12
|
+
app.config.middleware.insert(0, Eyeloupe::RequestMiddleware)
|
13
|
+
end
|
14
|
+
|
15
|
+
initializer "eyeloupe.importmap", :before => "importmap" do |app|
|
16
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
17
|
+
# https://github.com/rails/importmap-rails#sweeping-the-cache-in-development-and-test
|
18
|
+
app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'net/http'
|
3
|
+
module Net
|
4
|
+
class HTTP
|
5
|
+
alias original_request request
|
6
|
+
|
7
|
+
def request(req, body = nil, &block)
|
8
|
+
if Eyeloupe.configuration.capture
|
9
|
+
Eyeloupe::Processors::OutRequest.instance.init(req, body)
|
10
|
+
res = original_request(req, body, &block)
|
11
|
+
Eyeloupe::Processors::OutRequest.instance.process(res)
|
12
|
+
else
|
13
|
+
res = original_request(req, body, &block)
|
14
|
+
end
|
15
|
+
res
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|