dbwatcher 0.1.5 → 1.0.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 +2 -2
- data/app/controllers/dbwatcher/base_controller.rb +95 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +12 -0
- data/app/controllers/dbwatcher/queries_controller.rb +24 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +15 -20
- data/app/controllers/dbwatcher/tables_controller.rb +38 -0
- data/app/helpers/dbwatcher/application_helper.rb +103 -0
- data/app/helpers/dbwatcher/formatting_helper.rb +108 -0
- data/app/helpers/dbwatcher/session_helper.rb +27 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +177 -0
- data/app/views/dbwatcher/queries/index.html.erb +240 -0
- data/app/views/dbwatcher/sessions/index.html.erb +120 -27
- data/app/views/dbwatcher/sessions/show.html.erb +326 -129
- data/app/views/dbwatcher/shared/_badge.html.erb +4 -0
- data/app/views/dbwatcher/shared/_data_table.html.erb +20 -0
- data/app/views/dbwatcher/shared/_header.html.erb +7 -0
- data/app/views/dbwatcher/shared/_page_layout.html.erb +20 -0
- data/app/views/dbwatcher/shared/_section_panel.html.erb +9 -0
- data/app/views/dbwatcher/shared/_stats_card.html.erb +11 -0
- data/app/views/dbwatcher/shared/_tab_bar.html.erb +6 -0
- data/app/views/dbwatcher/tables/changes.html.erb +225 -0
- data/app/views/dbwatcher/tables/index.html.erb +123 -0
- data/app/views/dbwatcher/tables/show.html.erb +86 -0
- data/app/views/layouts/dbwatcher/application.html.erb +375 -26
- data/config/routes.rb +17 -3
- data/lib/dbwatcher/configuration.rb +9 -1
- data/lib/dbwatcher/engine.rb +12 -7
- data/lib/dbwatcher/logging.rb +72 -0
- data/lib/dbwatcher/services/dashboard_data_aggregator.rb +121 -0
- data/lib/dbwatcher/services/query_filter_processor.rb +114 -0
- data/lib/dbwatcher/services/table_statistics_collector.rb +119 -0
- data/lib/dbwatcher/sql_logger.rb +107 -0
- data/lib/dbwatcher/storage/api/base_api.rb +134 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +172 -0
- data/lib/dbwatcher/storage/api/query_api.rb +95 -0
- data/lib/dbwatcher/storage/api/session_api.rb +134 -0
- data/lib/dbwatcher/storage/api/table_api.rb +86 -0
- data/lib/dbwatcher/storage/base_storage.rb +113 -0
- data/lib/dbwatcher/storage/change_processor.rb +65 -0
- data/lib/dbwatcher/storage/concerns/data_normalizer.rb +134 -0
- data/lib/dbwatcher/storage/concerns/error_handler.rb +75 -0
- data/lib/dbwatcher/storage/concerns/timestampable.rb +74 -0
- data/lib/dbwatcher/storage/concerns/validatable.rb +117 -0
- data/lib/dbwatcher/storage/date_helper.rb +21 -0
- data/lib/dbwatcher/storage/errors.rb +86 -0
- data/lib/dbwatcher/storage/file_manager.rb +122 -0
- data/lib/dbwatcher/storage/null_session.rb +39 -0
- data/lib/dbwatcher/storage/query_storage.rb +338 -0
- data/lib/dbwatcher/storage/query_validator.rb +24 -0
- data/lib/dbwatcher/storage/session.rb +58 -0
- data/lib/dbwatcher/storage/session_operations.rb +37 -0
- data/lib/dbwatcher/storage/session_query.rb +71 -0
- data/lib/dbwatcher/storage/session_storage.rb +322 -0
- data/lib/dbwatcher/storage/table_storage.rb +237 -0
- data/lib/dbwatcher/storage.rb +112 -85
- data/lib/dbwatcher/tracker.rb +4 -55
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +12 -2
- metadata +47 -1
@@ -1,34 +1,383 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
2
|
<html>
|
3
|
-
<head>
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
<
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
3
|
+
<head>
|
4
|
+
<title>DB Watcher</title>
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
6
|
+
<%= csrf_meta_tags %>
|
7
|
+
|
8
|
+
<!-- Alpine.js -->
|
9
|
+
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
10
|
+
|
11
|
+
<!-- Tailwind CSS -->
|
12
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
13
|
+
|
14
|
+
<!-- Custom styles for compact theme -->
|
15
|
+
<style>
|
16
|
+
:root {
|
17
|
+
--navy-dark: #00285D;
|
18
|
+
--blue-light: #96C1E7;
|
19
|
+
--blue-medium: #6CADDF;
|
20
|
+
--gold-dark: #D4A11E;
|
21
|
+
--gold-light: #FFC758;
|
22
|
+
}
|
23
|
+
|
24
|
+
/* Compact table styles */
|
25
|
+
.compact-table {
|
26
|
+
font-family: 'Consolas', 'Monaco', 'Lucida Console', monospace;
|
27
|
+
font-size: 12px;
|
28
|
+
line-height: 1.2;
|
29
|
+
}
|
30
|
+
|
31
|
+
.compact-table th {
|
32
|
+
padding: 4px 8px;
|
33
|
+
font-weight: 500;
|
34
|
+
text-transform: none;
|
35
|
+
font-size: 11px;
|
36
|
+
background: #f3f3f3;
|
37
|
+
border-bottom: 2px solid #e8e8e8;
|
38
|
+
border-right: 1px solid #e8e8e8;
|
39
|
+
position: sticky;
|
40
|
+
top: 0;
|
41
|
+
z-index: 10;
|
42
|
+
}
|
43
|
+
|
44
|
+
.compact-table td {
|
45
|
+
padding: 2px 8px;
|
46
|
+
border-right: 1px solid #f0f0f0;
|
47
|
+
border-bottom: 1px solid #f0f0f0;
|
48
|
+
max-width: 200px;
|
49
|
+
overflow: hidden;
|
50
|
+
text-overflow: ellipsis;
|
51
|
+
white-space: nowrap;
|
52
|
+
}
|
53
|
+
|
54
|
+
.compact-table tr:hover {
|
55
|
+
background: #f8f8f8;
|
56
|
+
}
|
57
|
+
|
58
|
+
.compact-table tr.selected {
|
59
|
+
background: #e6f3ff;
|
60
|
+
}
|
61
|
+
|
62
|
+
/* Sidebar styles */
|
63
|
+
.sidebar-item {
|
64
|
+
font-size: 13px;
|
65
|
+
padding: 6px 12px;
|
66
|
+
display: flex;
|
67
|
+
align-items: center;
|
68
|
+
gap: 8px;
|
69
|
+
border-radius: 3px;
|
70
|
+
transition: all 0.15s;
|
71
|
+
}
|
72
|
+
|
73
|
+
.sidebar-item:hover {
|
74
|
+
background: rgba(108, 173, 223, 0.1);
|
75
|
+
color: #6CADDF;
|
76
|
+
}
|
77
|
+
|
78
|
+
.sidebar-item.active {
|
79
|
+
background: #00285D;
|
80
|
+
color: white;
|
81
|
+
}
|
82
|
+
|
83
|
+
/* Compact form controls */
|
84
|
+
.compact-input {
|
85
|
+
padding: 3px 8px;
|
86
|
+
font-size: 12px;
|
87
|
+
border: 1px solid #d1d5db;
|
88
|
+
border-radius: 3px;
|
89
|
+
}
|
90
|
+
|
91
|
+
.compact-select {
|
92
|
+
padding: 3px 24px 3px 8px;
|
93
|
+
font-size: 12px;
|
94
|
+
border: 1px solid #d1d5db;
|
95
|
+
border-radius: 3px;
|
96
|
+
background-size: 16px;
|
97
|
+
}
|
98
|
+
|
99
|
+
.compact-button {
|
100
|
+
padding: 4px 12px;
|
101
|
+
font-size: 12px;
|
102
|
+
border-radius: 3px;
|
103
|
+
font-weight: 500;
|
104
|
+
}
|
105
|
+
|
106
|
+
/* Tab styles */
|
107
|
+
.tab-bar {
|
108
|
+
background: #f3f3f3;
|
109
|
+
border-bottom: 1px solid #e8e8e8;
|
110
|
+
display: flex;
|
111
|
+
align-items: center;
|
112
|
+
height: 32px;
|
113
|
+
font-size: 12px;
|
114
|
+
}
|
115
|
+
|
116
|
+
.tab-item {
|
117
|
+
padding: 0 16px;
|
118
|
+
height: 100%;
|
119
|
+
display: flex;
|
120
|
+
align-items: center;
|
121
|
+
border-right: 1px solid #e8e8e8;
|
122
|
+
cursor: pointer;
|
123
|
+
transition: all 0.15s;
|
124
|
+
}
|
125
|
+
|
126
|
+
.tab-item:hover {
|
127
|
+
background: #e8e8e8;
|
128
|
+
}
|
129
|
+
|
130
|
+
.tab-item.active {
|
131
|
+
background: white;
|
132
|
+
color: #00285D;
|
133
|
+
font-weight: 500;
|
134
|
+
}
|
135
|
+
|
136
|
+
/* Status badges */
|
137
|
+
.badge-insert { background: #10b981; color: white; }
|
138
|
+
.badge-update { background: #6CADDF; color: white; }
|
139
|
+
.badge-delete { background: #ef4444; color: white; }
|
140
|
+
.badge-select { background: #6b7280; color: white; }
|
141
|
+
|
142
|
+
.badge {
|
143
|
+
padding: 1px 6px;
|
144
|
+
font-size: 10px;
|
145
|
+
border-radius: 2px;
|
146
|
+
font-weight: 500;
|
147
|
+
text-transform: uppercase;
|
148
|
+
}
|
149
|
+
|
150
|
+
/* Highlight colors */
|
151
|
+
.highlight-change { background: rgba(255, 199, 88, 0.3); }
|
152
|
+
.highlight-new { background: rgba(16, 185, 129, 0.2); }
|
153
|
+
.highlight-deleted { background: rgba(239, 68, 68, 0.2); }
|
154
|
+
|
155
|
+
/* Splitter */
|
156
|
+
.splitter {
|
157
|
+
width: 4px;
|
158
|
+
background: #e8e8e8;
|
159
|
+
cursor: col-resize;
|
160
|
+
}
|
161
|
+
|
162
|
+
.splitter:hover {
|
163
|
+
background: #6CADDF;
|
164
|
+
}
|
165
|
+
|
166
|
+
/* Scrollbar styling */
|
167
|
+
::-webkit-scrollbar {
|
168
|
+
width: 8px;
|
169
|
+
height: 8px;
|
170
|
+
}
|
171
|
+
|
172
|
+
::-webkit-scrollbar-track {
|
173
|
+
background: #f3f3f3;
|
174
|
+
}
|
175
|
+
|
176
|
+
::-webkit-scrollbar-thumb {
|
177
|
+
background: #c8c8c8;
|
178
|
+
border-radius: 4px;
|
179
|
+
}
|
180
|
+
|
181
|
+
::-webkit-scrollbar-thumb:hover {
|
182
|
+
background: #6CADDF;
|
183
|
+
}
|
184
|
+
|
185
|
+
/* Enhanced table styles for data readability */
|
186
|
+
.table-detailed .compact-table td {
|
187
|
+
max-width: 300px;
|
188
|
+
padding: 4px 8px;
|
189
|
+
}
|
190
|
+
|
191
|
+
.cell-content {
|
192
|
+
position: relative;
|
193
|
+
display: inline-block;
|
194
|
+
max-width: 100%;
|
195
|
+
}
|
196
|
+
|
197
|
+
.cell-compact {
|
198
|
+
max-width: 150px;
|
199
|
+
overflow: hidden;
|
200
|
+
text-overflow: ellipsis;
|
201
|
+
white-space: nowrap;
|
202
|
+
}
|
203
|
+
|
204
|
+
.cell-detailed {
|
205
|
+
max-width: 300px;
|
206
|
+
white-space: pre-wrap;
|
207
|
+
word-break: break-word;
|
208
|
+
}
|
209
|
+
|
210
|
+
.cell-value {
|
211
|
+
cursor: help;
|
212
|
+
}
|
213
|
+
|
214
|
+
/* Column type styling */
|
215
|
+
.column-meta {
|
216
|
+
background-color: rgba(156, 163, 175, 0.1);
|
217
|
+
}
|
218
|
+
|
219
|
+
.column-timestamp {
|
220
|
+
background-color: rgba(59, 130, 246, 0.1);
|
221
|
+
font-family: monospace;
|
222
|
+
}
|
223
|
+
|
224
|
+
.column-id {
|
225
|
+
background-color: rgba(245, 158, 11, 0.1);
|
226
|
+
font-family: monospace;
|
227
|
+
}
|
228
|
+
|
229
|
+
/* Tooltip improvements */
|
230
|
+
.tooltip-content {
|
231
|
+
max-height: 200px;
|
232
|
+
overflow-y: auto;
|
233
|
+
font-family: 'Consolas', 'Monaco', 'Lucida Console', monospace;
|
234
|
+
}
|
235
|
+
|
236
|
+
/* Essential mode adjustments */
|
237
|
+
.view-essential .compact-table th,
|
238
|
+
.view-essential .compact-table td {
|
239
|
+
font-size: 11px;
|
240
|
+
padding: 2px 6px;
|
241
|
+
}
|
242
|
+
|
243
|
+
/* JSON/Array content indicators */
|
244
|
+
.json-indicator {
|
245
|
+
color: #6366f1;
|
246
|
+
font-style: italic;
|
247
|
+
font-size: 10px;
|
248
|
+
}
|
249
|
+
|
250
|
+
.array-indicator {
|
251
|
+
color: #059669;
|
252
|
+
font-style: italic;
|
253
|
+
font-size: 10px;
|
254
|
+
}
|
255
|
+
</style>
|
256
|
+
|
257
|
+
<script>
|
258
|
+
tailwind.config = {
|
259
|
+
theme: {
|
260
|
+
extend: {
|
261
|
+
colors: {
|
262
|
+
'navy-dark': '#00285D',
|
263
|
+
'blue-light': '#96C1E7',
|
264
|
+
'blue-medium': '#6CADDF',
|
265
|
+
'gold-dark': '#D4A11E',
|
266
|
+
'gold-light': '#FFC758',
|
267
|
+
}
|
268
|
+
}
|
269
|
+
}
|
270
|
+
}
|
271
|
+
</script>
|
272
|
+
</head>
|
273
|
+
|
274
|
+
<body class="bg-gray-50 h-screen overflow-hidden text-gray-800">
|
275
|
+
<div class="flex h-full" x-data="{ sidebarWidth: 200, sidebarCollapsed: false }">
|
276
|
+
<!-- Compact Sidebar -->
|
277
|
+
<aside class="bg-gray-900 text-gray-300 flex-shrink-0 transition-all duration-200"
|
278
|
+
:style="{ width: sidebarCollapsed ? '48px' : sidebarWidth + 'px' }">
|
279
|
+
<div class="flex flex-col h-full">
|
280
|
+
<!-- Logo -->
|
281
|
+
<div class="h-10 flex items-center justify-between px-3 border-b border-gray-700">
|
282
|
+
<div class="flex items-center gap-2" x-show="!sidebarCollapsed">
|
283
|
+
<svg class="w-5 h-5 text-gold-light" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
284
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
285
|
+
</svg>
|
286
|
+
<span class="text-sm font-medium text-white">DB Watcher</span>
|
287
|
+
</div>
|
288
|
+
<button @click="sidebarCollapsed = !sidebarCollapsed"
|
289
|
+
class="p-1 hover:bg-gray-800 rounded text-gray-400">
|
290
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
291
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
292
|
+
:d="sidebarCollapsed ? 'M13 5l7 7-7 7M5 5l7 7-7 7' : 'M11 19l-7-7 7-7m8 14l-7-7 7-7'"/>
|
293
|
+
</svg>
|
294
|
+
</button>
|
17
295
|
</div>
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
296
|
+
|
297
|
+
<!-- Navigation -->
|
298
|
+
<nav class="flex-1 py-2 overflow-y-auto">
|
299
|
+
<%= link_to root_path, class: "sidebar-item #{current_page?(root_path) ? 'active' : ''}" do %>
|
300
|
+
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
301
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
302
|
+
d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>
|
303
|
+
</svg>
|
304
|
+
<span x-show="!sidebarCollapsed">Dashboard</span>
|
305
|
+
<% end %>
|
306
|
+
|
307
|
+
<%= link_to sessions_path, class: "sidebar-item #{current_page?(sessions_path) ? 'active' : ''}" do %>
|
308
|
+
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
309
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
310
|
+
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
311
|
+
</svg>
|
312
|
+
<span x-show="!sidebarCollapsed">Sessions</span>
|
313
|
+
<% end %>
|
314
|
+
|
315
|
+
<%= link_to tables_path, class: "sidebar-item #{current_page?(tables_path) ? 'active' : ''}" do %>
|
316
|
+
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
317
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
318
|
+
d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
319
|
+
</svg>
|
320
|
+
<span x-show="!sidebarCollapsed">Tables</span>
|
321
|
+
<% end %>
|
322
|
+
|
323
|
+
<%= link_to queries_path, class: "sidebar-item #{current_page?(queries_path) ? 'active' : ''}" do %>
|
324
|
+
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
325
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
326
|
+
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
327
|
+
</svg>
|
328
|
+
<span x-show="!sidebarCollapsed">SQL Logs</span>
|
329
|
+
<% end %>
|
330
|
+
</nav>
|
331
|
+
|
332
|
+
<!-- Actions -->
|
333
|
+
<div class="p-2 border-t border-gray-700">
|
334
|
+
<!-- TODO: Add more action later -->
|
24
335
|
</div>
|
25
336
|
</div>
|
337
|
+
</aside>
|
338
|
+
|
339
|
+
<!-- Splitter -->
|
340
|
+
<div class="splitter" x-show="!sidebarCollapsed"
|
341
|
+
@mousedown="startResize($event)"
|
342
|
+
x-data="{
|
343
|
+
startResize(e) {
|
344
|
+
const startX = e.pageX;
|
345
|
+
const startWidth = sidebarWidth;
|
346
|
+
|
347
|
+
const doDrag = (e) => {
|
348
|
+
sidebarWidth = Math.max(150, Math.min(400, startWidth + e.pageX - startX));
|
349
|
+
};
|
350
|
+
|
351
|
+
const stopDrag = () => {
|
352
|
+
document.removeEventListener('mousemove', doDrag);
|
353
|
+
document.removeEventListener('mouseup', stopDrag);
|
354
|
+
};
|
355
|
+
|
356
|
+
document.addEventListener('mousemove', doDrag);
|
357
|
+
document.addEventListener('mouseup', stopDrag);
|
358
|
+
}
|
359
|
+
}">
|
26
360
|
</div>
|
27
|
-
</nav>
|
28
361
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
</
|
362
|
+
<!-- Main Content -->
|
363
|
+
<main class="flex-1 overflow-hidden bg-white">
|
364
|
+
<%= yield %>
|
365
|
+
</main>
|
366
|
+
</div>
|
367
|
+
|
368
|
+
<script>
|
369
|
+
mermaid.initialize({
|
370
|
+
startOnLoad: true,
|
371
|
+
theme: 'neutral',
|
372
|
+
themeVariables: {
|
373
|
+
primaryColor: '#00285D',
|
374
|
+
primaryTextColor: '#fff',
|
375
|
+
primaryBorderColor: '#6CADDF',
|
376
|
+
lineColor: '#96C1E7',
|
377
|
+
secondaryColor: '#FFC758',
|
378
|
+
tertiaryColor: '#D4A11E'
|
379
|
+
}
|
380
|
+
});
|
381
|
+
</script>
|
382
|
+
</body>
|
34
383
|
</html>
|
data/config/routes.rb
CHANGED
@@ -1,10 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
Dbwatcher::Engine.routes.draw do
|
4
|
-
|
4
|
+
root to: "dashboard#index"
|
5
|
+
|
6
|
+
resources :sessions do
|
7
|
+
collection do
|
8
|
+
delete :clear
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
resources :tables, only: %i[index show] do
|
13
|
+
member do
|
14
|
+
get :changes
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
resources :queries, only: [:index] do
|
5
19
|
collection do
|
6
|
-
|
20
|
+
get :filter
|
21
|
+
delete :clear
|
7
22
|
end
|
8
23
|
end
|
9
|
-
root to: "sessions#index"
|
10
24
|
end
|
@@ -2,13 +2,21 @@
|
|
2
2
|
|
3
3
|
module Dbwatcher
|
4
4
|
class Configuration
|
5
|
-
attr_accessor :storage_path, :enabled, :max_sessions, :auto_clean_after_days
|
5
|
+
attr_accessor :storage_path, :enabled, :max_sessions, :auto_clean_after_days,
|
6
|
+
:track_queries, :slow_query_threshold, :max_query_logs_per_day,
|
7
|
+
:mount_path
|
6
8
|
|
7
9
|
def initialize
|
8
10
|
@storage_path = default_storage_path
|
9
11
|
@enabled = true
|
10
12
|
@max_sessions = 100
|
11
13
|
@auto_clean_after_days = 7
|
14
|
+
@mount_path = "/dbwatcher"
|
15
|
+
|
16
|
+
# SQL Query tracking
|
17
|
+
@track_queries = true
|
18
|
+
@slow_query_threshold = 100.0 # milliseconds
|
19
|
+
@max_query_logs_per_day = 10_000
|
12
20
|
end
|
13
21
|
|
14
22
|
private
|
data/lib/dbwatcher/engine.rb
CHANGED
@@ -5,18 +5,23 @@ module Dbwatcher
|
|
5
5
|
isolate_namespace Dbwatcher
|
6
6
|
|
7
7
|
initializer "dbwatcher.setup" do |app|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
if Dbwatcher.configuration.enabled && !Rails.env.production?
|
9
|
+
# Auto-include in all models
|
10
|
+
ActiveSupport.on_load(:active_record) do
|
11
|
+
include Dbwatcher::ModelExtension
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add middleware
|
15
|
+
app.middleware.use Dbwatcher::Middleware
|
12
16
|
|
13
|
-
|
14
|
-
|
17
|
+
# Setup SQL logging if enabled
|
18
|
+
Dbwatcher::SqlLogger.instance if Dbwatcher.configuration.track_queries
|
19
|
+
end
|
15
20
|
end
|
16
21
|
|
17
22
|
# Mount the engine routes automatically
|
18
23
|
initializer "dbwatcher.routes", after: :add_routing_paths do |app|
|
19
|
-
app.routes.
|
24
|
+
app.routes.prepend do
|
20
25
|
mount Dbwatcher::Engine => "/dbwatcher", as: :dbwatcher
|
21
26
|
end
|
22
27
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
# Logging module for DBWatcher components
|
5
|
+
# Provides consistent logging interface across all services and components
|
6
|
+
module Logging
|
7
|
+
extend ActiveSupport::Concern if defined?(ActiveSupport)
|
8
|
+
|
9
|
+
# Log an informational message with optional context
|
10
|
+
# @param message [String] the log message
|
11
|
+
# @param context [Hash] additional context data
|
12
|
+
def log_info(message, context = {})
|
13
|
+
log_with_level(:info, message, context)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Log a debug message with optional context
|
17
|
+
# @param message [String] the log message
|
18
|
+
# @param context [Hash] additional context data
|
19
|
+
def log_debug(message, context = {})
|
20
|
+
log_with_level(:debug, message, context)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Log a warning message with optional context
|
24
|
+
# @param message [String] the log message
|
25
|
+
# @param context [Hash] additional context data
|
26
|
+
def log_warn(message, context = {})
|
27
|
+
log_with_level(:warn, message, context)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Log an error message with optional context
|
31
|
+
# @param message [String] the log message
|
32
|
+
# @param context [Hash] additional context data
|
33
|
+
def log_error(message, context = {})
|
34
|
+
log_with_level(:error, message, context)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def log_with_level(level, message, context)
|
40
|
+
logger = rails_logger || fallback_logger
|
41
|
+
formatted_message = format_log_message(message, context)
|
42
|
+
logger.public_send(level, formatted_message)
|
43
|
+
end
|
44
|
+
|
45
|
+
def format_log_message(message, context)
|
46
|
+
base_message = "[DBWatcher:#{component_name}] #{message}"
|
47
|
+
return base_message if context.empty?
|
48
|
+
|
49
|
+
context_string = context.map { |k, v| "#{k}=#{v}" }.join(" ")
|
50
|
+
"#{base_message} | #{context_string}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def component_name
|
54
|
+
self.class.name.split("::").last
|
55
|
+
end
|
56
|
+
|
57
|
+
def rails_logger
|
58
|
+
return nil unless defined?(Rails)
|
59
|
+
|
60
|
+
Rails.logger
|
61
|
+
end
|
62
|
+
|
63
|
+
def fallback_logger
|
64
|
+
@fallback_logger ||= Logger.new($stdout).tap do |logger|
|
65
|
+
logger.level = Logger::INFO
|
66
|
+
logger.formatter = proc do |severity, datetime, _progname, msg|
|
67
|
+
"#{datetime.strftime("%Y-%m-%d %H:%M:%S")} [#{severity}] #{msg}\n"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
# Service object for aggregating dashboard statistics
|
6
|
+
# Provides recent sessions, active tables, and query metrics
|
7
|
+
class DashboardDataAggregator
|
8
|
+
include Dbwatcher::Logging
|
9
|
+
|
10
|
+
# @return [Hash] dashboard statistics with recent_sessions, active_tables, query_stats
|
11
|
+
def self.call
|
12
|
+
new.call
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
log_info "Starting dashboard data aggregation"
|
17
|
+
start_time = Time.current
|
18
|
+
|
19
|
+
result = {
|
20
|
+
recent_sessions: fetch_recent_sessions,
|
21
|
+
active_tables: calculate_active_tables,
|
22
|
+
query_stats: aggregate_query_statistics
|
23
|
+
}
|
24
|
+
|
25
|
+
duration = Time.current - start_time
|
26
|
+
log_info "Completed dashboard aggregation in #{duration.round(3)}s", {
|
27
|
+
recent_sessions_count: result[:recent_sessions].length,
|
28
|
+
active_tables_count: result[:active_tables].length,
|
29
|
+
total_queries: result[:query_stats][:total]
|
30
|
+
}
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# @return [Array<Hash>] most recent 5 sessions
|
38
|
+
def fetch_recent_sessions
|
39
|
+
Storage.sessions.all.first(5)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Array<Array>] top 10 most active tables with change counts
|
43
|
+
def calculate_active_tables
|
44
|
+
table_activity_counts = Hash.new(0)
|
45
|
+
|
46
|
+
Storage.sessions.all.first(10).each do |session_info|
|
47
|
+
session = Storage.sessions.find(session_info[:id])
|
48
|
+
next unless session
|
49
|
+
|
50
|
+
session.changes.each do |change|
|
51
|
+
table_name = change[:table_name]
|
52
|
+
table_activity_counts[table_name] += 1 if table_name
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
table_activity_counts
|
57
|
+
.sort_by { |_table, count| -count }
|
58
|
+
.first(10)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Hash] query statistics including totals and breakdowns
|
62
|
+
def aggregate_query_statistics
|
63
|
+
date = Date.current.strftime("%Y-%m-%d")
|
64
|
+
log_debug "Aggregating query statistics for date: #{date}"
|
65
|
+
|
66
|
+
queries = fetch_queries_for_date(date)
|
67
|
+
build_query_statistics(queries)
|
68
|
+
rescue StandardError => e
|
69
|
+
log_error "Failed to aggregate query statistics: #{e.message}"
|
70
|
+
default_query_statistics
|
71
|
+
end
|
72
|
+
|
73
|
+
def fetch_queries_for_date(date)
|
74
|
+
Storage.queries.for_date(date).all
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_query_statistics(queries)
|
78
|
+
slow_queries_count = count_slow_queries(queries)
|
79
|
+
operations_breakdown = group_queries_by_operation(queries)
|
80
|
+
|
81
|
+
log_query_statistics_summary(queries, slow_queries_count, operations_breakdown)
|
82
|
+
|
83
|
+
{
|
84
|
+
total: queries.count,
|
85
|
+
slow_queries: slow_queries_count,
|
86
|
+
by_operation: operations_breakdown
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def log_query_statistics_summary(queries, slow_queries_count, operations_breakdown)
|
91
|
+
log_debug "Query stats aggregated", {
|
92
|
+
total_queries: queries.count,
|
93
|
+
slow_queries: slow_queries_count,
|
94
|
+
operations: operations_breakdown.keys.join(", ")
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def count_slow_queries(queries)
|
99
|
+
queries.count { |query| query_is_slow?(query) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def query_is_slow?(query)
|
103
|
+
query["duration"] && query["duration"] > 100
|
104
|
+
end
|
105
|
+
|
106
|
+
def group_queries_by_operation(queries)
|
107
|
+
queries
|
108
|
+
.group_by { |query| query[:operation] || "UNKNOWN" }
|
109
|
+
.transform_values(&:count)
|
110
|
+
end
|
111
|
+
|
112
|
+
def default_query_statistics
|
113
|
+
{
|
114
|
+
total: 0,
|
115
|
+
slow_queries: 0,
|
116
|
+
by_operation: {}
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|