binocs 0.1.0 → 0.2.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/README.md +17 -4
- data/app/controllers/binocs/requests_controller.rb +190 -1
- data/app/helpers/binocs/application_helper.rb +43 -11
- data/app/models/binocs/request.rb +31 -0
- data/app/views/binocs/requests/_empty_list.html.erb +3 -3
- data/app/views/binocs/requests/_log_entry.html.erb +104 -0
- data/app/views/binocs/requests/_request.html.erb +5 -5
- data/app/views/binocs/requests/analytics.html.erb +239 -0
- data/app/views/binocs/requests/heatmap.html.erb +179 -0
- data/app/views/binocs/requests/index.html.erb +42 -37
- data/app/views/binocs/requests/lifecycle.html.erb +886 -0
- data/app/views/binocs/requests/raw.html.erb +184 -0
- data/app/views/binocs/requests/sequence.html.erb +449 -0
- data/app/views/binocs/requests/show.html.erb +81 -74
- data/app/views/layouts/binocs/application.html.erb +36 -19
- data/config/routes.rb +7 -0
- data/db/migrate/20260314000000_add_client_identifier_to_binocs_requests.rb +8 -0
- data/lib/binocs/engine.rb +9 -0
- data/lib/binocs/log_subscriber.rb +164 -0
- data/lib/binocs/middleware/request_recorder.rb +35 -3
- data/lib/binocs/tui/app.rb +181 -2
- data/lib/binocs/tui/endpoints.rb +327 -0
- data/lib/binocs/tui/help_screen.rb +16 -0
- data/lib/binocs/tui/sequence_diagram.rb +266 -0
- data/lib/binocs/tui/window.rb +20 -0
- data/lib/binocs/tui.rb +2 -0
- data/lib/binocs/version.rb +1 -1
- metadata +21 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div class="space-y-6">
|
|
2
2
|
<!-- Back Link -->
|
|
3
3
|
<div>
|
|
4
|
-
<%= link_to binocs.requests_path, class: "inline-flex items-center text-sm text-
|
|
4
|
+
<%= link_to binocs.requests_path, class: "inline-flex items-center text-sm text-zinc-500 hover:text-yellow-400 transition-colors" do %>
|
|
5
5
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
6
6
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
|
7
7
|
</svg>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
12
|
<!-- Header -->
|
|
13
|
-
<div class="bg-
|
|
13
|
+
<div class="bg-zinc-900 rounded-lg p-6 border border-yellow-500/20">
|
|
14
14
|
<div class="flex items-center justify-between">
|
|
15
15
|
<div class="flex items-center space-x-4">
|
|
16
16
|
<span class="inline-flex items-center rounded px-3 py-1.5 text-sm font-medium <%= method_badge_class(@request.method) %>">
|
|
@@ -19,45 +19,51 @@
|
|
|
19
19
|
<span class="inline-flex items-center rounded px-3 py-1.5 text-sm font-medium <%= status_badge_class(@request.status_code) %>">
|
|
20
20
|
<%= @request.status_code %>
|
|
21
21
|
</span>
|
|
22
|
-
<h1 class="text-lg font-semibold text-
|
|
22
|
+
<h1 class="text-lg font-semibold text-zinc-200 break-all"><%= @request.path %></h1>
|
|
23
23
|
</div>
|
|
24
24
|
<div class="flex items-center space-x-2">
|
|
25
|
-
<%= link_to
|
|
26
|
-
|
|
25
|
+
<%= link_to binocs.lifecycle_request_path(@request.uuid), class: "rounded-md bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30 px-3 py-2 text-sm font-medium hover:bg-yellow-500/20 transition-all inline-flex items-center", data: { testid: "lifecycle-link" } do %>
|
|
26
|
+
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
|
28
|
+
</svg>
|
|
29
|
+
Lifecycle
|
|
30
|
+
<% end %>
|
|
31
|
+
<%= link_to "Open in Swagger", @request.swagger_url, target: "_blank", class: "rounded-md bg-yellow-500 px-3 py-2 text-sm font-medium text-black hover:bg-yellow-400 transition-colors" %>
|
|
32
|
+
<%= button_to "Delete", binocs.request_path(@request.uuid), method: :delete, class: "rounded-md bg-rose-500/15 text-rose-400 ring-1 ring-rose-500/20 px-3 py-2 text-sm font-medium hover:bg-rose-500/25 transition-colors", data: { turbo_confirm: "Are you sure?" } %>
|
|
27
33
|
</div>
|
|
28
34
|
</div>
|
|
29
35
|
|
|
30
36
|
<div class="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
|
31
37
|
<div>
|
|
32
|
-
<dt class="text-sm text-
|
|
33
|
-
<dd class="text-lg font-medium text-
|
|
38
|
+
<dt class="text-sm text-zinc-500">Duration</dt>
|
|
39
|
+
<dd class="text-lg font-medium text-zinc-200"><%= @request.formatted_duration %></dd>
|
|
34
40
|
</div>
|
|
35
41
|
<div>
|
|
36
|
-
<dt class="text-sm text-
|
|
37
|
-
<dd class="text-lg font-medium text-
|
|
42
|
+
<dt class="text-sm text-zinc-500">Memory Delta</dt>
|
|
43
|
+
<dd class="text-lg font-medium text-zinc-200"><%= @request.formatted_memory_delta %></dd>
|
|
38
44
|
</div>
|
|
39
45
|
<div>
|
|
40
|
-
<dt class="text-sm text-
|
|
41
|
-
<dd class="text-lg font-medium text-
|
|
46
|
+
<dt class="text-sm text-zinc-500">IP Address</dt>
|
|
47
|
+
<dd class="text-lg font-medium text-zinc-200"><%= @request.ip_address || "N/A" %></dd>
|
|
42
48
|
</div>
|
|
43
49
|
<div>
|
|
44
|
-
<dt class="text-sm text-
|
|
45
|
-
<dd class="text-lg font-medium text-
|
|
50
|
+
<dt class="text-sm text-zinc-500">Time</dt>
|
|
51
|
+
<dd class="text-lg font-medium text-zinc-200"><%= @request.created_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
|
|
46
52
|
</div>
|
|
47
53
|
</div>
|
|
48
54
|
|
|
49
55
|
<% if @request.controller_action %>
|
|
50
|
-
<div class="mt-4 pt-4 border-t border-
|
|
51
|
-
<span class="text-sm text-
|
|
52
|
-
<span class="ml-2 text-sm text-
|
|
56
|
+
<div class="mt-4 pt-4 border-t border-zinc-800">
|
|
57
|
+
<span class="text-sm text-zinc-500">Route:</span>
|
|
58
|
+
<span class="ml-2 text-sm text-zinc-200"><%= @request.controller_action %></span>
|
|
53
59
|
</div>
|
|
54
60
|
<% end %>
|
|
55
61
|
</div>
|
|
56
62
|
|
|
57
63
|
<!-- Exception (if present) -->
|
|
58
64
|
<% if @request.has_exception? %>
|
|
59
|
-
<div class="bg-
|
|
60
|
-
<h2 class="text-lg font-semibold text-
|
|
65
|
+
<div class="bg-rose-500/5 rounded-lg p-6 border border-rose-500/20">
|
|
66
|
+
<h2 class="text-lg font-semibold text-rose-400 flex items-center">
|
|
61
67
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
62
68
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
|
63
69
|
</svg>
|
|
@@ -65,42 +71,55 @@
|
|
|
65
71
|
</h2>
|
|
66
72
|
<div class="mt-4 space-y-3">
|
|
67
73
|
<div>
|
|
68
|
-
<span class="text-sm text-
|
|
74
|
+
<span class="text-sm text-rose-300 font-medium"><%= @request.exception["class"] %></span>
|
|
69
75
|
</div>
|
|
70
|
-
<div class="text-sm text-
|
|
76
|
+
<div class="text-sm text-rose-200">
|
|
71
77
|
<%= @request.exception["message"] %>
|
|
72
78
|
</div>
|
|
73
79
|
<% if @request.exception["backtrace"].present? %>
|
|
74
80
|
<div class="mt-4">
|
|
75
|
-
<h3 class="text-sm font-medium text-
|
|
76
|
-
<pre class="bg-
|
|
81
|
+
<h3 class="text-sm font-medium text-rose-300 mb-2">Backtrace</h3>
|
|
82
|
+
<pre class="bg-zinc-950 rounded p-4 text-xs text-rose-200 overflow-x-auto max-h-96"><%= Array(@request.exception["backtrace"]).join("\n") %></pre>
|
|
77
83
|
</div>
|
|
78
84
|
<% end %>
|
|
85
|
+
<% cause = @request.exception["cause"] %>
|
|
86
|
+
<% depth = 0 %>
|
|
87
|
+
<% while cause.present? && depth < 5 %>
|
|
88
|
+
<div class="mt-4 pl-4 border-l-2 border-rose-500/30">
|
|
89
|
+
<h3 class="text-sm font-semibold text-rose-400 mb-1">Caused by: <%= cause["class"] %></h3>
|
|
90
|
+
<div class="text-sm text-rose-200"><%= cause["message"] %></div>
|
|
91
|
+
<% if cause["backtrace"].present? %>
|
|
92
|
+
<pre class="mt-2 bg-zinc-950 rounded p-3 text-xs text-rose-200/70 overflow-x-auto max-h-48"><%= Array(cause["backtrace"]).join("\n") %></pre>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
<% cause = cause["cause"] %>
|
|
96
|
+
<% depth += 1 %>
|
|
97
|
+
<% end %>
|
|
79
98
|
</div>
|
|
80
99
|
</div>
|
|
81
100
|
<% end %>
|
|
82
101
|
|
|
83
102
|
<!-- Tabs -->
|
|
84
|
-
<div x-data="{ activeTab: 'params' }" class="bg-
|
|
103
|
+
<div x-data="{ activeTab: 'params' }" class="bg-zinc-900 rounded-lg border border-zinc-800 overflow-hidden">
|
|
85
104
|
<!-- Tab Headers -->
|
|
86
|
-
<div class="border-b border-
|
|
105
|
+
<div class="border-b border-zinc-800">
|
|
87
106
|
<nav class="flex -mb-px">
|
|
88
|
-
<button @click="activeTab = 'params'" :class="activeTab === 'params' ? 'border-
|
|
107
|
+
<button @click="activeTab = 'params'" :class="activeTab === 'params' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
|
|
89
108
|
Params
|
|
90
109
|
</button>
|
|
91
|
-
<button @click="activeTab = 'headers'" :class="activeTab === 'headers' ? 'border-
|
|
110
|
+
<button @click="activeTab = 'headers'" :class="activeTab === 'headers' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
|
|
92
111
|
Headers
|
|
93
112
|
</button>
|
|
94
|
-
<button @click="activeTab = 'body'" :class="activeTab === 'body' ? 'border-
|
|
113
|
+
<button @click="activeTab = 'body'" :class="activeTab === 'body' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
|
|
95
114
|
Body
|
|
96
115
|
</button>
|
|
97
|
-
<button @click="activeTab = 'response'" :class="activeTab === 'response' ? 'border-
|
|
116
|
+
<button @click="activeTab = 'response'" :class="activeTab === 'response' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
|
|
98
117
|
Response
|
|
99
118
|
</button>
|
|
100
|
-
<button @click="activeTab = 'logs'" :class="activeTab === 'logs' ? 'border-
|
|
119
|
+
<button @click="activeTab = 'logs'" :class="activeTab === 'logs' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
|
|
101
120
|
Logs
|
|
102
121
|
<% if @request.logs.present? && @request.logs.any? %>
|
|
103
|
-
<span class="ml-2 inline-flex items-center rounded-full bg-
|
|
122
|
+
<span class="ml-2 inline-flex items-center rounded-full bg-zinc-800 px-2 py-0.5 text-xs text-zinc-400"><%= @request.logs.size %></span>
|
|
104
123
|
<% end %>
|
|
105
124
|
</button>
|
|
106
125
|
</nav>
|
|
@@ -114,13 +133,13 @@
|
|
|
114
133
|
<div class="space-y-2">
|
|
115
134
|
<% @request.params.each do |key, value| %>
|
|
116
135
|
<div class="flex">
|
|
117
|
-
<span class="text-sm font-medium text-
|
|
118
|
-
<span class="text-sm text-
|
|
136
|
+
<span class="text-sm font-medium text-yellow-400 w-48 flex-shrink-0"><%= key %></span>
|
|
137
|
+
<span class="text-sm text-zinc-200 break-all"><%= format_value(value) %></span>
|
|
119
138
|
</div>
|
|
120
139
|
<% end %>
|
|
121
140
|
</div>
|
|
122
141
|
<% else %>
|
|
123
|
-
<p class="text-sm text-
|
|
142
|
+
<p class="text-sm text-zinc-500">No parameters</p>
|
|
124
143
|
<% end %>
|
|
125
144
|
</div>
|
|
126
145
|
|
|
@@ -128,34 +147,34 @@
|
|
|
128
147
|
<div x-show="activeTab === 'headers'" x-cloak>
|
|
129
148
|
<div class="space-y-6">
|
|
130
149
|
<div>
|
|
131
|
-
<h3 class="text-sm font-medium text-
|
|
150
|
+
<h3 class="text-sm font-medium text-zinc-300 mb-3">Request Headers</h3>
|
|
132
151
|
<% if @request.request_headers.present? && @request.request_headers.any? %>
|
|
133
152
|
<div class="space-y-2">
|
|
134
153
|
<% @request.request_headers.each do |key, value| %>
|
|
135
154
|
<div class="flex">
|
|
136
|
-
<span class="text-sm font-medium text-
|
|
137
|
-
<span class="text-sm text-
|
|
155
|
+
<span class="text-sm font-medium text-yellow-400 w-48 flex-shrink-0"><%= key %></span>
|
|
156
|
+
<span class="text-sm text-zinc-200 break-all"><%= value %></span>
|
|
138
157
|
</div>
|
|
139
158
|
<% end %>
|
|
140
159
|
</div>
|
|
141
160
|
<% else %>
|
|
142
|
-
<p class="text-sm text-
|
|
161
|
+
<p class="text-sm text-zinc-500">No request headers</p>
|
|
143
162
|
<% end %>
|
|
144
163
|
</div>
|
|
145
164
|
|
|
146
165
|
<div>
|
|
147
|
-
<h3 class="text-sm font-medium text-
|
|
166
|
+
<h3 class="text-sm font-medium text-zinc-300 mb-3">Response Headers</h3>
|
|
148
167
|
<% if @request.response_headers.present? && @request.response_headers.any? %>
|
|
149
168
|
<div class="space-y-2">
|
|
150
169
|
<% @request.response_headers.each do |key, value| %>
|
|
151
170
|
<div class="flex">
|
|
152
|
-
<span class="text-sm font-medium text-
|
|
153
|
-
<span class="text-sm text-
|
|
171
|
+
<span class="text-sm font-medium text-yellow-400 w-48 flex-shrink-0"><%= key %></span>
|
|
172
|
+
<span class="text-sm text-zinc-200 break-all"><%= value %></span>
|
|
154
173
|
</div>
|
|
155
174
|
<% end %>
|
|
156
175
|
</div>
|
|
157
176
|
<% else %>
|
|
158
|
-
<p class="text-sm text-
|
|
177
|
+
<p class="text-sm text-zinc-500">No response headers</p>
|
|
159
178
|
<% end %>
|
|
160
179
|
</div>
|
|
161
180
|
</div>
|
|
@@ -163,65 +182,53 @@
|
|
|
163
182
|
|
|
164
183
|
<!-- Body Tab -->
|
|
165
184
|
<div x-show="activeTab === 'body'" x-cloak>
|
|
166
|
-
<h3 class="text-sm font-medium text-
|
|
185
|
+
<h3 class="text-sm font-medium text-zinc-300 mb-3">Request Body</h3>
|
|
167
186
|
<% if @request.request_body.present? %>
|
|
168
|
-
<pre class="bg-
|
|
187
|
+
<pre class="bg-zinc-950 rounded p-4 text-sm text-zinc-200 overflow-x-auto max-h-96"><%= format_body(@request.request_body) %></pre>
|
|
169
188
|
<% else %>
|
|
170
|
-
<p class="text-sm text-
|
|
189
|
+
<p class="text-sm text-zinc-500">No request body</p>
|
|
171
190
|
<% end %>
|
|
172
191
|
</div>
|
|
173
192
|
|
|
174
193
|
<!-- Response Tab -->
|
|
175
194
|
<div x-show="activeTab === 'response'" x-cloak>
|
|
176
|
-
<h3 class="text-sm font-medium text-
|
|
195
|
+
<h3 class="text-sm font-medium text-zinc-300 mb-3">Response Body</h3>
|
|
177
196
|
<% if @request.response_body.present? %>
|
|
178
|
-
<pre class="bg-
|
|
197
|
+
<pre class="bg-zinc-950 rounded p-4 text-sm text-zinc-200 overflow-x-auto max-h-96"><%= format_body(@request.response_body) %></pre>
|
|
179
198
|
<% else %>
|
|
180
|
-
<p class="text-sm text-
|
|
199
|
+
<p class="text-sm text-zinc-500">No response body captured</p>
|
|
181
200
|
<% end %>
|
|
182
201
|
</div>
|
|
183
202
|
|
|
184
203
|
<!-- Logs Tab -->
|
|
185
204
|
<div x-show="activeTab === 'logs'" x-cloak>
|
|
186
205
|
<% if @request.logs.present? && @request.logs.any? %>
|
|
206
|
+
<!-- Log type filter -->
|
|
207
|
+
<div class="mb-4 flex items-center space-x-2 flex-wrap" x-data="{ logFilter: 'all' }">
|
|
208
|
+
<span class="text-xs text-zinc-600 mr-1">Filter:</span>
|
|
209
|
+
<button @click="logFilter = 'all'" :class="logFilter === 'all' ? 'bg-yellow-500/15 text-yellow-400 ring-1 ring-yellow-500/30' : 'bg-zinc-800 text-zinc-500 hover:text-zinc-200'" class="rounded px-2 py-1 text-xs font-medium transition-colors">All (<%= @request.logs.size %>)</button>
|
|
210
|
+
<% log_types = @request.logs.map { |l| l["type"] }.tally.sort_by { |_, c| -c } %>
|
|
211
|
+
<% log_types.each do |type, count| %>
|
|
212
|
+
<button @click="logFilter = '<%= type %>'" :class="logFilter === '<%= type %>' ? 'bg-yellow-500/15 text-yellow-400 ring-1 ring-yellow-500/30' : 'bg-zinc-800 text-zinc-500 hover:text-zinc-200'" class="rounded px-2 py-1 text-xs font-medium transition-colors"><%= type %> (<%= count %>)</button>
|
|
213
|
+
<% end %>
|
|
214
|
+
</div>
|
|
187
215
|
<div class="space-y-3">
|
|
188
216
|
<% @request.logs.each do |log| %>
|
|
189
|
-
<div
|
|
190
|
-
|
|
191
|
-
<span class="text-xs font-medium text-indigo-400 uppercase"><%= log["type"] %></span>
|
|
192
|
-
<span class="text-xs text-slate-500"><%= log["timestamp"] %></span>
|
|
193
|
-
</div>
|
|
194
|
-
<% if log["type"] == "controller" %>
|
|
195
|
-
<div class="text-sm text-white">
|
|
196
|
-
<span class="text-slate-400">Controller:</span> <%= log["controller"] %>#<%= log["action"] %>
|
|
197
|
-
<% if log["view_runtime"] %>
|
|
198
|
-
<span class="ml-4 text-slate-400">View:</span> <%= log["view_runtime"] %>ms
|
|
199
|
-
<% end %>
|
|
200
|
-
<% if log["db_runtime"] %>
|
|
201
|
-
<span class="ml-4 text-slate-400">DB:</span> <%= log["db_runtime"] %>ms
|
|
202
|
-
<% end %>
|
|
203
|
-
</div>
|
|
204
|
-
<% elsif log["type"] == "redirect" %>
|
|
205
|
-
<div class="text-sm text-white">
|
|
206
|
-
<span class="text-slate-400">Redirect to:</span> <%= log["location"] %>
|
|
207
|
-
<span class="ml-4 text-slate-400">Status:</span> <%= log["status"] %>
|
|
208
|
-
</div>
|
|
209
|
-
<% else %>
|
|
210
|
-
<pre class="text-sm text-white"><%= format_value(log) %></pre>
|
|
211
|
-
<% end %>
|
|
217
|
+
<div x-show="logFilter === 'all' || logFilter === '<%= log["type"] %>'">
|
|
218
|
+
<%= render partial: "binocs/requests/log_entry", locals: { log: log } %>
|
|
212
219
|
</div>
|
|
213
220
|
<% end %>
|
|
214
221
|
</div>
|
|
215
222
|
<% else %>
|
|
216
|
-
<p class="text-sm text-
|
|
223
|
+
<p class="text-sm text-zinc-500">No logs captured</p>
|
|
217
224
|
<% end %>
|
|
218
225
|
</div>
|
|
219
226
|
</div>
|
|
220
227
|
</div>
|
|
221
228
|
|
|
222
229
|
<!-- Full URL -->
|
|
223
|
-
<div class="bg-
|
|
224
|
-
<h2 class="text-sm font-medium text-
|
|
225
|
-
<p class="text-sm text-
|
|
230
|
+
<div class="bg-zinc-900 rounded-lg p-6 border border-zinc-800">
|
|
231
|
+
<h2 class="text-sm font-medium text-zinc-500 mb-2">Full URL</h2>
|
|
232
|
+
<p class="text-sm text-zinc-200 break-all font-mono"><%= @request.full_url %></p>
|
|
226
233
|
</div>
|
|
227
234
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" class="h-full bg-
|
|
2
|
+
<html lang="en" class="h-full bg-zinc-950">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
extend: {
|
|
17
17
|
colors: {
|
|
18
18
|
binocs: {
|
|
19
|
-
bg: '#
|
|
20
|
-
card: '#
|
|
21
|
-
border: '#
|
|
22
|
-
accent: '#
|
|
19
|
+
bg: '#09090b',
|
|
20
|
+
card: '#18181b',
|
|
21
|
+
border: '#27272a',
|
|
22
|
+
accent: '#eab308',
|
|
23
|
+
gold: '#ca8a04'
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -33,20 +34,36 @@
|
|
|
33
34
|
<body class="h-full">
|
|
34
35
|
<div class="min-h-full">
|
|
35
36
|
<!-- Navigation -->
|
|
36
|
-
<nav class="bg-
|
|
37
|
+
<nav class="bg-zinc-950 border-b border-yellow-500/20">
|
|
37
38
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
38
39
|
<div class="flex h-16 items-center justify-between">
|
|
39
40
|
<div class="flex items-center">
|
|
40
|
-
<div class="flex-shrink-0">
|
|
41
|
-
|
|
42
|
-
<
|
|
41
|
+
<div class="flex-shrink-0 flex items-center">
|
|
42
|
+
<!-- Binoculars icon inspired by the Binocs brand icon -->
|
|
43
|
+
<svg class="w-7 h-7 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
44
|
+
<circle cx="8" cy="14" r="5" stroke="#eab308" stroke-width="1.5" fill="#eab308" fill-opacity="0.15"/>
|
|
45
|
+
<circle cx="16" cy="14" r="5" stroke="#eab308" stroke-width="1.5" fill="#eab308" fill-opacity="0.15"/>
|
|
46
|
+
<circle cx="8" cy="14" r="2.5" fill="#18181b" stroke="#fefce8" stroke-width="0.75"/>
|
|
47
|
+
<circle cx="16" cy="14" r="2.5" fill="#18181b" stroke="#fefce8" stroke-width="0.75"/>
|
|
48
|
+
<rect x="11" y="12" width="2" height="4" rx="1" fill="#eab308"/>
|
|
49
|
+
<path d="M6 9 L8 11" stroke="#eab308" stroke-width="1.5" stroke-linecap="round"/>
|
|
50
|
+
<path d="M18 9 L16 11" stroke="#eab308" stroke-width="1.5" stroke-linecap="round"/>
|
|
51
|
+
<!-- Cyan accent dots from brand icon -->
|
|
52
|
+
<circle cx="8" cy="14" r="0.75" fill="#22d3ee"/>
|
|
53
|
+
<circle cx="16" cy="14" r="0.75" fill="#22d3ee"/>
|
|
54
|
+
</svg>
|
|
55
|
+
<span class="text-2xl font-bold text-yellow-500">Binocs</span>
|
|
56
|
+
<span class="ml-2 text-sm text-zinc-500">Request Monitor</span>
|
|
43
57
|
</div>
|
|
44
58
|
<div class="ml-10 flex items-baseline space-x-4">
|
|
45
|
-
<%= link_to "Requests", binocs.requests_path, class: "rounded-md px-3 py-2 text-sm font-medium #{current_page?(binocs.requests_path) ? 'bg-
|
|
59
|
+
<%= link_to "Requests", binocs.requests_path, class: "rounded-md px-3 py-2 text-sm font-medium transition-all #{current_page?(binocs.requests_path) ? 'bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30' : 'text-zinc-400 hover:bg-zinc-800 hover:text-yellow-400'}" %>
|
|
60
|
+
<%= link_to "Sequence", binocs.sequence_requests_path, class: "rounded-md px-3 py-2 text-sm font-medium transition-all #{current_page?(binocs.sequence_requests_path) ? 'bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30' : 'text-zinc-400 hover:bg-zinc-800 hover:text-yellow-400'}" %>
|
|
61
|
+
<%= link_to "Heatmap", binocs.heatmap_requests_path, class: "rounded-md px-3 py-2 text-sm font-medium transition-all #{current_page?(binocs.heatmap_requests_path) ? 'bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30' : 'text-zinc-400 hover:bg-zinc-800 hover:text-yellow-400'}" %>
|
|
62
|
+
<%= link_to "Analytics", binocs.analytics_requests_path, class: "rounded-md px-3 py-2 text-sm font-medium transition-all #{current_page?(binocs.analytics_requests_path) ? 'bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30' : 'text-zinc-400 hover:bg-zinc-800 hover:text-yellow-400'}" %>
|
|
46
63
|
</div>
|
|
47
64
|
</div>
|
|
48
65
|
<div class="flex items-center space-x-4">
|
|
49
|
-
<span class="text-xs text-
|
|
66
|
+
<span class="text-xs text-zinc-600">
|
|
50
67
|
Rails <%= Rails::VERSION::STRING %> |
|
|
51
68
|
Ruby <%= RUBY_VERSION %> |
|
|
52
69
|
<%= Rails.env %>
|
|
@@ -57,7 +74,7 @@
|
|
|
57
74
|
</nav>
|
|
58
75
|
|
|
59
76
|
<!-- Authentication Required Banner (hidden by default, shown by JS when WebSocket fails) -->
|
|
60
|
-
<div id="binocs-auth-banner" class="hidden bg-amber-900/
|
|
77
|
+
<div id="binocs-auth-banner" class="hidden bg-amber-900/30 border-b border-amber-700/50">
|
|
61
78
|
<div class="mx-auto max-w-7xl px-4 py-3 sm:px-6 lg:px-8">
|
|
62
79
|
<div class="flex items-center justify-between flex-wrap gap-2">
|
|
63
80
|
<div class="flex items-center">
|
|
@@ -71,10 +88,10 @@
|
|
|
71
88
|
</div>
|
|
72
89
|
<div class="flex gap-2">
|
|
73
90
|
<% login_url = "#{Binocs.configuration.resolved_login_path}?redirect_to=#{ERB::Util.url_encode(request.fullpath)}" rescue Binocs.configuration.resolved_login_path %>
|
|
74
|
-
<a href="<%= login_url %>" class="rounded-md bg-
|
|
91
|
+
<a href="<%= login_url %>" class="rounded-md bg-yellow-500 px-3 py-1.5 text-sm font-medium text-black hover:bg-yellow-400 transition-colors">
|
|
75
92
|
Sign in
|
|
76
93
|
</a>
|
|
77
|
-
<button onclick="document.getElementById('binocs-auth-banner').classList.add('hidden')" class="rounded-md bg-
|
|
94
|
+
<button onclick="document.getElementById('binocs-auth-banner').classList.add('hidden')" class="rounded-md bg-zinc-800 px-3 py-1.5 text-sm font-medium text-zinc-300 hover:bg-zinc-700 transition-colors">
|
|
78
95
|
Dismiss
|
|
79
96
|
</button>
|
|
80
97
|
</div>
|
|
@@ -84,22 +101,22 @@
|
|
|
84
101
|
|
|
85
102
|
<!-- Flash Messages -->
|
|
86
103
|
<% if notice %>
|
|
87
|
-
<div class="bg-
|
|
104
|
+
<div class="bg-emerald-900/30 border-b border-emerald-800/50">
|
|
88
105
|
<div class="mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8">
|
|
89
|
-
<p class="text-sm text-
|
|
106
|
+
<p class="text-sm text-emerald-300"><%= notice %></p>
|
|
90
107
|
</div>
|
|
91
108
|
</div>
|
|
92
109
|
<% end %>
|
|
93
110
|
<% if alert %>
|
|
94
|
-
<div class="bg-
|
|
111
|
+
<div class="bg-rose-900/30 border-b border-rose-800/50">
|
|
95
112
|
<div class="mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8">
|
|
96
|
-
<p class="text-sm text-
|
|
113
|
+
<p class="text-sm text-rose-300"><%= alert %></p>
|
|
97
114
|
</div>
|
|
98
115
|
</div>
|
|
99
116
|
<% end %>
|
|
100
117
|
|
|
101
118
|
<!-- Main Content -->
|
|
102
|
-
<main class="bg-
|
|
119
|
+
<main class="bg-zinc-950 min-h-screen">
|
|
103
120
|
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
104
121
|
<%= yield %>
|
|
105
122
|
</div>
|
data/config/routes.rb
CHANGED
|
@@ -4,8 +4,15 @@ Binocs::Engine.routes.draw do
|
|
|
4
4
|
root to: "requests#index"
|
|
5
5
|
|
|
6
6
|
resources :requests, only: [:index, :show, :destroy] do
|
|
7
|
+
member do
|
|
8
|
+
get :lifecycle
|
|
9
|
+
get :raw
|
|
10
|
+
end
|
|
7
11
|
collection do
|
|
8
12
|
delete :clear
|
|
13
|
+
get :sequence
|
|
14
|
+
get :heatmap
|
|
15
|
+
get :analytics
|
|
9
16
|
end
|
|
10
17
|
end
|
|
11
18
|
end
|
data/lib/binocs/engine.rb
CHANGED
|
@@ -25,6 +25,15 @@ module Binocs
|
|
|
25
25
|
|
|
26
26
|
require_relative "log_subscriber"
|
|
27
27
|
Binocs::LogSubscriber.attach_to :action_controller
|
|
28
|
+
Binocs::SqlLogSubscriber.attach_to :active_record
|
|
29
|
+
Binocs::ViewLogSubscriber.attach_to :action_view
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
initializer "binocs.log_interceptor", after: :initialize_logger do
|
|
33
|
+
next unless Binocs.enabled?
|
|
34
|
+
|
|
35
|
+
require_relative "log_subscriber"
|
|
36
|
+
Rails.logger = Binocs::LogInterceptor.new(Rails.logger)
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
initializer "binocs.assets" do |app|
|
|
@@ -20,6 +20,26 @@ module Binocs
|
|
|
20
20
|
duration: event.duration.round(2),
|
|
21
21
|
timestamp: Time.current.iso8601
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
# Capture exception info from the payload if present
|
|
25
|
+
if payload[:exception_object]
|
|
26
|
+
ex = payload[:exception_object]
|
|
27
|
+
Thread.current[:binocs_logs] << {
|
|
28
|
+
type: "exception",
|
|
29
|
+
class: ex.class.name,
|
|
30
|
+
message: ex.message,
|
|
31
|
+
backtrace: ex.backtrace&.first(30),
|
|
32
|
+
cause: exception_cause_chain(ex),
|
|
33
|
+
timestamp: Time.current.iso8601
|
|
34
|
+
}
|
|
35
|
+
elsif payload[:exception]
|
|
36
|
+
Thread.current[:binocs_logs] << {
|
|
37
|
+
type: "exception",
|
|
38
|
+
class: payload[:exception].first,
|
|
39
|
+
message: payload[:exception].second,
|
|
40
|
+
timestamp: Time.current.iso8601
|
|
41
|
+
}
|
|
42
|
+
end
|
|
23
43
|
end
|
|
24
44
|
|
|
25
45
|
def halted_callback(event)
|
|
@@ -52,5 +72,149 @@ module Binocs
|
|
|
52
72
|
timestamp: Time.current.iso8601
|
|
53
73
|
}
|
|
54
74
|
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def exception_cause_chain(ex, depth = 0)
|
|
79
|
+
return nil if ex.cause.nil? || depth >= 5
|
|
80
|
+
|
|
81
|
+
cause = ex.cause
|
|
82
|
+
{
|
|
83
|
+
class: cause.class.name,
|
|
84
|
+
message: cause.message,
|
|
85
|
+
backtrace: cause.backtrace&.first(10),
|
|
86
|
+
cause: exception_cause_chain(cause, depth + 1)
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Captures ActiveRecord SQL queries during a request
|
|
92
|
+
class SqlLogSubscriber < ActiveSupport::LogSubscriber
|
|
93
|
+
# Only capture queries that are meaningful for debugging
|
|
94
|
+
IGNORED_QUERIES = %w[SCHEMA EXPLAIN].freeze
|
|
95
|
+
|
|
96
|
+
def sql(event)
|
|
97
|
+
return unless Thread.current[:binocs_logs]
|
|
98
|
+
|
|
99
|
+
payload = event.payload
|
|
100
|
+
return if IGNORED_QUERIES.include?(payload[:name])
|
|
101
|
+
return if payload[:name] == "CACHE"
|
|
102
|
+
|
|
103
|
+
entry = {
|
|
104
|
+
type: "sql",
|
|
105
|
+
name: payload[:name] || "SQL",
|
|
106
|
+
sql: truncate_sql(payload[:sql]),
|
|
107
|
+
duration: event.duration.round(2),
|
|
108
|
+
timestamp: Time.current.iso8601
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Flag queries that are likely part of an error
|
|
112
|
+
if payload[:exception]
|
|
113
|
+
entry[:error] = true
|
|
114
|
+
entry[:exception_class] = payload[:exception].first
|
|
115
|
+
entry[:exception_message] = payload[:exception].second
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Thread.current[:binocs_logs] << entry
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def truncate_sql(sql)
|
|
124
|
+
return nil if sql.nil?
|
|
125
|
+
|
|
126
|
+
sql = sql.squish
|
|
127
|
+
sql.length > 2000 ? "#{sql[0, 2000]}..." : sql
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Captures ActionView template and partial renders
|
|
132
|
+
class ViewLogSubscriber < ActiveSupport::LogSubscriber
|
|
133
|
+
def render_template(event)
|
|
134
|
+
record_render(event, "template")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def render_partial(event)
|
|
138
|
+
record_render(event, "partial")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def render_layout(event)
|
|
142
|
+
record_render(event, "layout")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def record_render(event, render_type)
|
|
148
|
+
return unless Thread.current[:binocs_logs]
|
|
149
|
+
|
|
150
|
+
payload = event.payload
|
|
151
|
+
identifier = payload[:identifier]
|
|
152
|
+
# Make the path relative to Rails root for readability
|
|
153
|
+
if identifier && defined?(Rails.root)
|
|
154
|
+
identifier = identifier.sub("#{Rails.root}/", "")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
entry = {
|
|
158
|
+
type: "render",
|
|
159
|
+
render_type: render_type,
|
|
160
|
+
identifier: identifier,
|
|
161
|
+
duration: event.duration.round(2),
|
|
162
|
+
timestamp: Time.current.iso8601
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if payload[:exception]
|
|
166
|
+
entry[:error] = true
|
|
167
|
+
entry[:exception_class] = payload[:exception].first
|
|
168
|
+
entry[:exception_message] = payload[:exception].second
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
Thread.current[:binocs_logs] << entry
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Intercepts Rails.logger output during a request to capture log lines
|
|
176
|
+
class LogInterceptor < Logger
|
|
177
|
+
def initialize(original_logger)
|
|
178
|
+
@original_logger = original_logger
|
|
179
|
+
super(nil)
|
|
180
|
+
self.level = original_logger.level
|
|
181
|
+
self.formatter = original_logger.formatter
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def add(severity, message = nil, progname = nil, &block)
|
|
185
|
+
# Always pass through to original logger
|
|
186
|
+
@original_logger.add(severity, message, progname, &block)
|
|
187
|
+
|
|
188
|
+
# Capture to binocs logs if we're in a request context and severity >= WARN
|
|
189
|
+
if Thread.current[:binocs_logs] && severity && severity >= Logger::WARN
|
|
190
|
+
msg = message || (block ? block.call : progname)
|
|
191
|
+
return true if msg.nil? || msg.to_s.strip.empty?
|
|
192
|
+
|
|
193
|
+
level_name = case severity
|
|
194
|
+
when Logger::WARN then "warn"
|
|
195
|
+
when Logger::ERROR then "error"
|
|
196
|
+
when Logger::FATAL then "fatal"
|
|
197
|
+
else "unknown"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
Thread.current[:binocs_logs] << {
|
|
201
|
+
type: "log",
|
|
202
|
+
level: level_name,
|
|
203
|
+
message: msg.to_s[0, 4000],
|
|
204
|
+
timestamp: Time.current.iso8601
|
|
205
|
+
}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
true
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Delegate everything else to the original logger
|
|
212
|
+
def method_missing(method, *args, &block)
|
|
213
|
+
@original_logger.send(method, *args, &block)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def respond_to_missing?(method, include_private = false)
|
|
217
|
+
@original_logger.respond_to?(method, include_private) || super
|
|
218
|
+
end
|
|
55
219
|
end
|
|
56
220
|
end
|