catpm 0.6.5 → 0.7.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 +1 -1
- data/app/controllers/catpm/application_controller.rb +15 -0
- data/app/controllers/catpm/system_controller.rb +4 -0
- data/app/helpers/catpm/application_helper.rb +142 -0
- data/app/views/catpm/system/index.html.erb +76 -479
- data/app/views/catpm/system/pipeline.html.erb +344 -0
- data/app/views/layouts/catpm/application.html.erb +40 -0
- data/app/views/layouts/catpm/pipeline.html.erb +79 -0
- data/config/routes.rb +1 -0
- data/lib/catpm/buffer.rb +22 -2
- data/lib/catpm/call_tracer.rb +32 -9
- data/lib/catpm/flusher.rb +44 -63
- data/lib/catpm/request_segments.rb +6 -1
- data/lib/catpm/version.rb +1 -1
- metadata +3 -1
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
<div class="pipeline-showcase">
|
|
2
|
+
<div class="pipeline">
|
|
3
|
+
<div class="pipeline-node">
|
|
4
|
+
<div class="node-icon"><svg width="36" height="36" viewBox="0 0 28 28" fill="none" stroke="var(--text-2)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="14" cy="14" r="9"/><path d="M14 9v5l3.5 3.5"/></svg></div>
|
|
5
|
+
<div class="node-label">Capture</div>
|
|
6
|
+
<div class="node-value">Middleware</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="pipeline-arrow">
|
|
9
|
+
<svg width="24" height="16" viewBox="0 0 24 16"><path d="M0 8h20M16 3l5 5-5 5" stroke="var(--text-2)" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="pipeline-node">
|
|
12
|
+
<div class="node-icon"><svg width="36" height="36" viewBox="0 0 28 28" fill="none" stroke="var(--text-2)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="16" height="20" rx="2"/><line x1="10" y1="10" x2="18" y2="10"/><line x1="10" y1="14" x2="18" y2="14"/><line x1="10" y1="18" x2="15" y2="18"/></svg></div>
|
|
13
|
+
<div class="node-label">Buffer</div>
|
|
14
|
+
<div class="node-value">Memory Queue</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="pipeline-arrow">
|
|
17
|
+
<svg width="24" height="16" viewBox="0 0 24 16"><path d="M0 8h20M16 3l5 5-5 5" stroke="var(--text-2)" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="pipeline-node">
|
|
20
|
+
<div class="node-icon"><svg width="36" height="36" viewBox="0 0 28 28" fill="none" stroke="var(--text-2)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,22 10,16 15,19 24,8"/><polyline points="18,8 24,8 24,14"/></svg></div>
|
|
21
|
+
<div class="node-label">Flush</div>
|
|
22
|
+
<div class="node-value">Aggregator</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="pipeline-arrow">
|
|
25
|
+
<svg width="24" height="16" viewBox="0 0 24 16"><path d="M0 8h20M16 3l5 5-5 5" stroke="var(--text-2)" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="pipeline-node">
|
|
28
|
+
<div class="node-icon"><svg width="36" height="36" viewBox="0 0 28 28" fill="none" stroke="var(--text-2)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="14" cy="8" rx="8" ry="4"/><path d="M6 8v12c0 2.2 3.58 4 8 4s8-1.8 8-4V8"/><path d="M6 14c0 2.2 3.58 4 8 4s8-1.8 8-4"/></svg></div>
|
|
29
|
+
<div class="node-label">Database</div>
|
|
30
|
+
<div class="node-value">Storage</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
(function() {
|
|
37
|
+
var el = document.querySelector('.pipeline');
|
|
38
|
+
if (!el) return;
|
|
39
|
+
|
|
40
|
+
var pnodes = el.querySelectorAll('.pipeline-node');
|
|
41
|
+
if (pnodes.length < 4) return;
|
|
42
|
+
var r0 = pnodes[0].getBoundingClientRect();
|
|
43
|
+
var r1 = pnodes[1].getBoundingClientRect();
|
|
44
|
+
if (r1.top > r0.bottom - 10) return;
|
|
45
|
+
|
|
46
|
+
var arrows = el.querySelectorAll('.pipeline-arrow');
|
|
47
|
+
|
|
48
|
+
var canvas = document.createElement('canvas');
|
|
49
|
+
canvas.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:2';
|
|
50
|
+
el.style.position = 'relative';
|
|
51
|
+
el.style.overflow = 'hidden';
|
|
52
|
+
el.appendChild(canvas);
|
|
53
|
+
|
|
54
|
+
var ctx = canvas.getContext('2d');
|
|
55
|
+
var dpr = window.devicePixelRatio || 1;
|
|
56
|
+
var W, H;
|
|
57
|
+
var FONT = '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif';
|
|
58
|
+
|
|
59
|
+
function resize() {
|
|
60
|
+
W = el.offsetWidth; H = el.offsetHeight;
|
|
61
|
+
canvas.width = W * dpr; canvas.height = H * dpr;
|
|
62
|
+
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
|
|
63
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
64
|
+
}
|
|
65
|
+
resize();
|
|
66
|
+
if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
|
|
67
|
+
|
|
68
|
+
function zones() {
|
|
69
|
+
var pr = el.getBoundingClientRect();
|
|
70
|
+
var nodeZones = Array.prototype.map.call(pnodes, function(n) {
|
|
71
|
+
var r = n.getBoundingClientRect();
|
|
72
|
+
return {
|
|
73
|
+
l: r.left - pr.left, r: r.right - pr.left,
|
|
74
|
+
t: r.top - pr.top, b: r.bottom - pr.top,
|
|
75
|
+
cx: (r.left + r.right) / 2 - pr.left,
|
|
76
|
+
cy: (r.top + r.bottom) / 2 - pr.top,
|
|
77
|
+
w: r.width, h: r.height
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
var arrowZones = Array.prototype.map.call(arrows, function(a) {
|
|
81
|
+
var r = a.getBoundingClientRect();
|
|
82
|
+
return {
|
|
83
|
+
l: r.left - pr.left, r: r.right - pr.left,
|
|
84
|
+
cx: (r.left + r.right) / 2 - pr.left,
|
|
85
|
+
cy: (r.top + r.bottom) / 2 - pr.top
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
return { nodes: nodeZones, arrows: arrowZones };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var C = { ok: '#1a7f37', error: '#cf222e', slow: '#9a6700', sample: '#8250df' };
|
|
92
|
+
var POOL = ['ok','ok','ok','ok','ok','ok','ok','slow','slow','error','sample'];
|
|
93
|
+
var SZ = 5;
|
|
94
|
+
var MAX_BUF = 36;
|
|
95
|
+
var FLUSH_MS = 3500;
|
|
96
|
+
|
|
97
|
+
var METHODS = ['GET','GET','GET','GET','POST','POST','PUT','PATCH','DELETE'];
|
|
98
|
+
var RPATHS = ['/users','/home','/orders','/api','/settings',
|
|
99
|
+
'/reports','/data','/search','/login','/pay','/items','/stats'];
|
|
100
|
+
|
|
101
|
+
function randLabel(type) {
|
|
102
|
+
var m = METHODS[Math.floor(Math.random() * METHODS.length)];
|
|
103
|
+
var p = RPATHS[Math.floor(Math.random() * RPATHS.length)];
|
|
104
|
+
if (type === 'slow') return m + ' ' + p + ' ' + (200 + Math.floor(Math.random() * 600)) + 'ms';
|
|
105
|
+
if (type === 'error') {
|
|
106
|
+
var codes = [500, 502, 503, 422];
|
|
107
|
+
return m + ' ' + p + ' ' + codes[Math.floor(Math.random() * codes.length)];
|
|
108
|
+
}
|
|
109
|
+
return m + ' ' + p;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function rrect(x, y, w, h, r) {
|
|
113
|
+
ctx.beginPath();
|
|
114
|
+
ctx.moveTo(x + r, y);
|
|
115
|
+
ctx.lineTo(x + w - r, y);
|
|
116
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
117
|
+
ctx.lineTo(x + w, y + h - r);
|
|
118
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
119
|
+
ctx.lineTo(x + r, y + h);
|
|
120
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
121
|
+
ctx.lineTo(x, y + r);
|
|
122
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
123
|
+
ctx.closePath();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function lerp(a, b, t) { return a + (b - a) * t; }
|
|
127
|
+
|
|
128
|
+
var particles = [];
|
|
129
|
+
var batchAcc = 0;
|
|
130
|
+
var flushAcc = 0;
|
|
131
|
+
var prev = 0;
|
|
132
|
+
|
|
133
|
+
function randType() { return POOL[Math.floor(Math.random() * POOL.length)]; }
|
|
134
|
+
|
|
135
|
+
function bufRowsPerCol(bz) { return Math.max(1, Math.floor((bz.h - 20) / (SZ + 2))); }
|
|
136
|
+
function bufSlot(idx, bz) {
|
|
137
|
+
var perCol = bufRowsPerCol(bz);
|
|
138
|
+
var col = Math.floor(idx / perCol);
|
|
139
|
+
var row = idx % perCol;
|
|
140
|
+
return {
|
|
141
|
+
x: bz.r - 10 - col * (SZ + 2) - SZ / 2,
|
|
142
|
+
y: bz.b - 10 - row * (SZ + 2) - SZ / 2
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function tick(t) {
|
|
147
|
+
if (!prev) { prev = t; requestAnimationFrame(tick); return; }
|
|
148
|
+
var dt = Math.min(t - prev, 50);
|
|
149
|
+
prev = t;
|
|
150
|
+
|
|
151
|
+
var z = zones();
|
|
152
|
+
if (z.nodes.length < 4) { requestAnimationFrame(tick); return; }
|
|
153
|
+
var cap = z.nodes[0], buf = z.nodes[1], flu = z.nodes[2], db = z.nodes[3];
|
|
154
|
+
var laneY = cap.t + cap.h * 0.42;
|
|
155
|
+
|
|
156
|
+
var bc = 0;
|
|
157
|
+
for (var i = 0; i < particles.length; i++) if (particles[i].state === 1) bc++;
|
|
158
|
+
|
|
159
|
+
var captureOccupied = false;
|
|
160
|
+
for (var i = 0; i < particles.length; i++) {
|
|
161
|
+
if (particles[i].state === 0 && particles[i].phase <= 1) {
|
|
162
|
+
captureOccupied = true; break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
batchAcc += dt;
|
|
167
|
+
if (batchAcc > 2500 + Math.random() * 1500 && !captureOccupied) {
|
|
168
|
+
batchAcc = 0;
|
|
169
|
+
var count = Math.min(1 + Math.floor(Math.random() * 5), MAX_BUF - bc);
|
|
170
|
+
if (count > 0) {
|
|
171
|
+
var spacing = 26;
|
|
172
|
+
var yBase = laneY - (count - 1) * spacing / 2;
|
|
173
|
+
for (var j = 0; j < count; j++) {
|
|
174
|
+
var type = randType();
|
|
175
|
+
var label = randLabel(type);
|
|
176
|
+
ctx.font = '500 11px ' + FONT;
|
|
177
|
+
var lw = ctx.measureText(label).width;
|
|
178
|
+
var s = bufSlot(bc + j, buf);
|
|
179
|
+
particles.push({
|
|
180
|
+
type: type, label: label, labelW: lw,
|
|
181
|
+
x: cap.l - 20,
|
|
182
|
+
y: yBase + j * spacing,
|
|
183
|
+
phase: 0,
|
|
184
|
+
lingerTime: 300 + Math.random() * 400,
|
|
185
|
+
lingerAcc: 0,
|
|
186
|
+
state: 0, opacity: 0, age: 0, progress: 0,
|
|
187
|
+
slotX: s.x, slotY: s.y
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
flushAcc += dt;
|
|
194
|
+
if (flushAcc > FLUSH_MS + Math.random() * 2000) {
|
|
195
|
+
flushAcc = 0;
|
|
196
|
+
var delay = 0;
|
|
197
|
+
for (var i = 0; i < particles.length; i++) {
|
|
198
|
+
var p = particles[i];
|
|
199
|
+
if (p.state === 1) {
|
|
200
|
+
p.state = 2;
|
|
201
|
+
p.phase = 0;
|
|
202
|
+
p.fx = flu.cx + (Math.random() - 0.5) * 24;
|
|
203
|
+
p.fy = flu.t + 18 + Math.random() * (flu.h * 0.35);
|
|
204
|
+
p.tx = db.cx + (Math.random() - 0.5) * 40;
|
|
205
|
+
p.ty = db.t + 18 + Math.random() * (db.h * 0.4);
|
|
206
|
+
p.delay = delay;
|
|
207
|
+
p.waited = 0;
|
|
208
|
+
p.pauseAcc = 0;
|
|
209
|
+
delay += 20 + Math.random() * 15;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (var i = particles.length - 1; i >= 0; i--) {
|
|
215
|
+
var p = particles[i];
|
|
216
|
+
|
|
217
|
+
if (p.state === 0) {
|
|
218
|
+
p.age = (p.age || 0) + dt;
|
|
219
|
+
|
|
220
|
+
if (p.phase === 0) {
|
|
221
|
+
p.opacity = Math.min(0.9, p.opacity + dt * 0.005);
|
|
222
|
+
var dx = cap.cx - p.x;
|
|
223
|
+
p.x += dx * 0.008 * dt;
|
|
224
|
+
if (Math.abs(dx) < 3) {
|
|
225
|
+
p.phase = 1;
|
|
226
|
+
p.x = cap.cx;
|
|
227
|
+
p.lingerAcc = 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (p.phase === 1) {
|
|
231
|
+
p.opacity = 0.9;
|
|
232
|
+
p.lingerAcc += dt;
|
|
233
|
+
if (p.lingerAcc > p.lingerTime) {
|
|
234
|
+
p.phase = 2;
|
|
235
|
+
p.progress = 0;
|
|
236
|
+
p.startX = p.x;
|
|
237
|
+
p.startY = p.y;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else if (p.phase === 2) {
|
|
241
|
+
p.progress = Math.min(1, p.progress + dt * 0.0012);
|
|
242
|
+
var ease = p.progress * p.progress * (3 - 2 * p.progress);
|
|
243
|
+
p.x = lerp(p.startX, p.slotX, ease);
|
|
244
|
+
p.y = lerp(p.startY, p.slotY, ease);
|
|
245
|
+
if (p.progress >= 1) {
|
|
246
|
+
p.state = 1;
|
|
247
|
+
p.x = p.slotX;
|
|
248
|
+
p.y = p.slotY;
|
|
249
|
+
p.opacity = 0.8;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (p.state === 1) {
|
|
254
|
+
p.x = p.slotX;
|
|
255
|
+
p.y = p.slotY;
|
|
256
|
+
}
|
|
257
|
+
else if (p.state === 2) {
|
|
258
|
+
p.waited = (p.waited || 0) + dt;
|
|
259
|
+
if (p.waited < p.delay) continue;
|
|
260
|
+
|
|
261
|
+
if (p.phase === 0) {
|
|
262
|
+
var dx = p.fx - p.x, dy = p.fy - p.y;
|
|
263
|
+
if (Math.abs(dx) < 6 && Math.abs(dy) < 6) {
|
|
264
|
+
p.phase = 1; p.pauseAcc = 0;
|
|
265
|
+
} else {
|
|
266
|
+
p.x += dx * 0.0035 * dt;
|
|
267
|
+
p.y += dy * 0.0035 * dt;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else if (p.phase === 1) {
|
|
271
|
+
p.pauseAcc += dt;
|
|
272
|
+
if (p.pauseAcc > 350 + Math.random() * 200) p.phase = 2;
|
|
273
|
+
}
|
|
274
|
+
else if (p.phase === 2) {
|
|
275
|
+
var dx = p.tx - p.x, dy = p.ty - p.y;
|
|
276
|
+
if (Math.abs(dx) < 4 && Math.abs(dy) < 4) {
|
|
277
|
+
p.state = 3; p.ds = t;
|
|
278
|
+
} else {
|
|
279
|
+
p.x += dx * 0.0035 * dt;
|
|
280
|
+
p.y += dy * 0.0035 * dt;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else if (p.state === 3) {
|
|
285
|
+
var e = t - p.ds;
|
|
286
|
+
p.opacity = 0.8 * Math.max(0, 1 - e / 800);
|
|
287
|
+
if (p.opacity <= 0.01) { particles.splice(i, 1); continue; }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
ctx.clearRect(0, 0, W, H);
|
|
292
|
+
for (var i = 0; i < particles.length; i++) {
|
|
293
|
+
var p = particles[i];
|
|
294
|
+
if (p.opacity <= 0.01) continue;
|
|
295
|
+
|
|
296
|
+
var scale = 0;
|
|
297
|
+
if (p.state === 0) {
|
|
298
|
+
if (p.phase <= 1) {
|
|
299
|
+
scale = 1;
|
|
300
|
+
} else {
|
|
301
|
+
scale = Math.max(0, 1 - p.progress * 1.5);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
ctx.globalAlpha = p.opacity;
|
|
306
|
+
|
|
307
|
+
if (scale > 0.12 && p.label) {
|
|
308
|
+
var fs = Math.round(11 * scale);
|
|
309
|
+
var bw = (p.labelW + 14) * scale;
|
|
310
|
+
var bh = 22 * scale;
|
|
311
|
+
var rad = 3 * scale;
|
|
312
|
+
|
|
313
|
+
ctx.fillStyle = C[p.type];
|
|
314
|
+
ctx.globalAlpha = p.opacity * 0.92;
|
|
315
|
+
rrect(p.x - bw / 2, p.y - bh / 2, bw, bh, rad);
|
|
316
|
+
ctx.fill();
|
|
317
|
+
|
|
318
|
+
if (fs >= 7) {
|
|
319
|
+
ctx.globalAlpha = p.opacity;
|
|
320
|
+
ctx.fillStyle = '#fff';
|
|
321
|
+
ctx.font = '500 ' + fs + 'px ' + FONT;
|
|
322
|
+
ctx.textAlign = 'center';
|
|
323
|
+
ctx.textBaseline = 'middle';
|
|
324
|
+
ctx.fillText(p.label, p.x, p.y + 0.5);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
ctx.fillStyle = C[p.type];
|
|
328
|
+
if (p.state === 3) {
|
|
329
|
+
var e = t - p.ds;
|
|
330
|
+
var s = SZ * (1 + e * 0.002);
|
|
331
|
+
ctx.fillRect(p.x - s / 2, p.y - s / 2, s, s);
|
|
332
|
+
} else {
|
|
333
|
+
ctx.fillRect(p.x - SZ / 2, p.y - SZ / 2, SZ, SZ);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
ctx.globalAlpha = 1;
|
|
338
|
+
|
|
339
|
+
requestAnimationFrame(tick);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
requestAnimationFrame(tick);
|
|
343
|
+
})();
|
|
344
|
+
</script>
|
|
@@ -290,6 +290,41 @@
|
|
|
290
290
|
|
|
291
291
|
.footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid var(--border); color: var(--text-2); font-size: 12px; text-align: center; }
|
|
292
292
|
|
|
293
|
+
/* ─── Diagnostics Grid ─── */
|
|
294
|
+
.diag-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 24px; }
|
|
295
|
+
.diag-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; }
|
|
296
|
+
.diag-label { font-size: 11px; color: var(--text-1); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; }
|
|
297
|
+
.diag-value { font-size: 22px; font-weight: 600; color: var(--text-0); margin-top: 2px; line-height: 1.2; }
|
|
298
|
+
.diag-value--warn { color: var(--yellow); }
|
|
299
|
+
.diag-unit { font-size: 12px; font-weight: 400; color: var(--text-2); }
|
|
300
|
+
.diag-detail { font-size: 12px; color: var(--text-2); margin-top: 4px; }
|
|
301
|
+
|
|
302
|
+
/* ─── Storage Card ─── */
|
|
303
|
+
.storage-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; margin-bottom: 24px; }
|
|
304
|
+
.storage-header { display: flex; align-items: baseline; gap: 10px; margin-bottom: 14px; }
|
|
305
|
+
.storage-total { font-size: 18px; font-weight: 600; color: var(--text-0); }
|
|
306
|
+
.storage-meta { font-size: 13px; color: var(--text-2); }
|
|
307
|
+
.storage-bars { display: flex; flex-direction: column; gap: 6px; }
|
|
308
|
+
.storage-row { display: grid; grid-template-columns: 120px 1fr auto auto; gap: 10px; align-items: center; font-size: 12px; }
|
|
309
|
+
.storage-name { color: var(--text-1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
310
|
+
.storage-bar-track { height: 6px; background: var(--bg-2); border-radius: 3px; overflow: hidden; }
|
|
311
|
+
.storage-bar-fill { height: 100%; background: var(--accent); border-radius: 3px; opacity: 0.6; transition: width 0.3s; }
|
|
312
|
+
.storage-row-stat { color: var(--text-1); text-align: right; min-width: 50px; }
|
|
313
|
+
.storage-row-size { color: var(--text-2); text-align: right; min-width: 60px; }
|
|
314
|
+
|
|
315
|
+
/* ─── Config Groups (System page) ─── */
|
|
316
|
+
.config-group { border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 12px; overflow: hidden; }
|
|
317
|
+
.config-group-title { font-size: 11px; font-weight: 600; color: var(--text-1); text-transform: uppercase; letter-spacing: 0.5px; padding: 10px 16px; margin: 0; background: var(--bg-1); border-bottom: 1px solid var(--border); }
|
|
318
|
+
.config-group-body { padding: 0; }
|
|
319
|
+
.config-item { padding: 10px 16px; border-bottom: 1px solid var(--bg-2); }
|
|
320
|
+
.config-item:last-child { border-bottom: none; }
|
|
321
|
+
.config-item-header { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
322
|
+
.config-item-label { font-weight: 500; font-size: 13px; color: var(--text-0); }
|
|
323
|
+
.config-item-value { font-size: 13px; color: var(--text-0); text-align: right; flex-shrink: 0; }
|
|
324
|
+
.config-item-meta { margin-top: 3px; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
|
|
325
|
+
.config-param { font-family: var(--font-mono); font-size: 11px; color: var(--accent); background: var(--bg-1); padding: 1px 5px; border-radius: 3px; white-space: nowrap; }
|
|
326
|
+
.config-desc { font-size: 12px; color: var(--text-2); line-height: 1.4; }
|
|
327
|
+
|
|
293
328
|
/* ─── Table Scroll ─── */
|
|
294
329
|
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
295
330
|
|
|
@@ -306,6 +341,11 @@
|
|
|
306
341
|
.pipeline-node { min-width: auto; }
|
|
307
342
|
.pipeline-arrow { justify-content: center; padding: 4px 0; }
|
|
308
343
|
.pipeline-arrow svg { transform: rotate(90deg); }
|
|
344
|
+
.config-item-header { flex-direction: column; gap: 2px; }
|
|
345
|
+
.config-item-value { text-align: left; }
|
|
346
|
+
.diag-grid { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
|
|
347
|
+
.storage-row { grid-template-columns: 90px 1fr auto auto; gap: 6px; }
|
|
348
|
+
.storage-header { flex-direction: column; gap: 2px; }
|
|
309
349
|
}
|
|
310
350
|
</style>
|
|
311
351
|
</head>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>catpm — Pipeline</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<style>
|
|
7
|
+
:root {
|
|
8
|
+
--bg-0: #ffffff;
|
|
9
|
+
--bg-1: #f6f8fa;
|
|
10
|
+
--bg-2: #eaeef2;
|
|
11
|
+
--border: #d0d7de;
|
|
12
|
+
--text-0: #1f2328;
|
|
13
|
+
--text-1: #656d76;
|
|
14
|
+
--text-2: #8b949e;
|
|
15
|
+
--accent: #0969da;
|
|
16
|
+
--red: #cf222e;
|
|
17
|
+
--green: #1a7f37;
|
|
18
|
+
--yellow: #9a6700;
|
|
19
|
+
--purple: #8250df;
|
|
20
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
21
|
+
--font-mono: "SF Mono", "Fira Code", Consolas, "Liberation Mono", Menlo, monospace;
|
|
22
|
+
--radius: 6px;
|
|
23
|
+
}
|
|
24
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
25
|
+
body {
|
|
26
|
+
font-family: var(--font-sans);
|
|
27
|
+
background: var(--bg-0);
|
|
28
|
+
color: var(--text-0);
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
min-height: 100vh;
|
|
33
|
+
margin: 0;
|
|
34
|
+
padding: 48px;
|
|
35
|
+
}
|
|
36
|
+
.pipeline-showcase { max-width: 1060px; width: 100%; }
|
|
37
|
+
.pipeline-showcase .pipeline {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: stretch;
|
|
40
|
+
gap: 0;
|
|
41
|
+
}
|
|
42
|
+
.pipeline-node {
|
|
43
|
+
background: var(--bg-1);
|
|
44
|
+
border: 1px solid var(--border);
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
padding: 28px 24px 24px;
|
|
47
|
+
text-align: center;
|
|
48
|
+
min-width: 200px;
|
|
49
|
+
flex: 1;
|
|
50
|
+
}
|
|
51
|
+
.pipeline-node .node-icon { margin-bottom: 10px; line-height: 1; opacity: 0.5; }
|
|
52
|
+
.pipeline-node .node-icon svg { width: 36px; height: 36px; }
|
|
53
|
+
.pipeline-node .node-label {
|
|
54
|
+
font-size: 12px;
|
|
55
|
+
color: var(--text-1);
|
|
56
|
+
text-transform: uppercase;
|
|
57
|
+
letter-spacing: 1px;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
}
|
|
60
|
+
.pipeline-node .node-value {
|
|
61
|
+
font-size: 15px;
|
|
62
|
+
font-weight: 500;
|
|
63
|
+
color: var(--text-2);
|
|
64
|
+
margin-top: 6px;
|
|
65
|
+
}
|
|
66
|
+
.pipeline-arrow {
|
|
67
|
+
color: var(--text-2);
|
|
68
|
+
padding: 0 8px;
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
}
|
|
72
|
+
@keyframes flow-arrow { 0% { opacity: 0.3; } 50% { opacity: 1; } 100% { opacity: 0.3; } }
|
|
73
|
+
.pipeline-arrow svg { animation: flow-arrow 1.5s ease-in-out infinite; }
|
|
74
|
+
</style>
|
|
75
|
+
</head>
|
|
76
|
+
<body>
|
|
77
|
+
<%= yield %>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
data/config/routes.rb
CHANGED
|
@@ -4,6 +4,7 @@ Catpm::Engine.routes.draw do
|
|
|
4
4
|
root 'status#index'
|
|
5
5
|
resources :status, only: [:index]
|
|
6
6
|
resources :system, only: [:index]
|
|
7
|
+
get 'pipeline', to: 'system#pipeline', as: :pipeline
|
|
7
8
|
get 'endpoint', to: 'endpoints#show', as: :endpoint
|
|
8
9
|
delete 'endpoint', to: 'endpoints#destroy'
|
|
9
10
|
patch 'endpoint/pin', to: 'endpoints#toggle_pin', as: :endpoint_pin
|
data/lib/catpm/buffer.rb
CHANGED
|
@@ -10,14 +10,29 @@ module Catpm
|
|
|
10
10
|
@current_bytes = 0
|
|
11
11
|
@max_bytes = max_bytes
|
|
12
12
|
@dropped_count = 0
|
|
13
|
+
@flush_callback = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Register a callback invoked when buffer reaches configured capacity.
|
|
17
|
+
# Used by Flusher to wake up immediately for an emergency flush.
|
|
18
|
+
def on_flush_needed(&block)
|
|
19
|
+
@flush_callback = block
|
|
13
20
|
end
|
|
14
21
|
|
|
15
22
|
# Called from request threads. Returns :accepted or :dropped.
|
|
16
23
|
# Never blocks — monitoring must not slow down the application.
|
|
24
|
+
#
|
|
25
|
+
# When buffer reaches max_bytes, signals the flusher for immediate drain
|
|
26
|
+
# and continues accepting events. Only drops as a last resort at 3x capacity
|
|
27
|
+
# (flusher stuck or DB down).
|
|
17
28
|
def push(event)
|
|
29
|
+
signal_flush = false
|
|
30
|
+
|
|
18
31
|
@monitor.synchronize do
|
|
19
32
|
bytes = event.estimated_bytes
|
|
20
|
-
|
|
33
|
+
|
|
34
|
+
# Hard safety cap: 3x configured limit prevents OOM if flusher is stuck
|
|
35
|
+
if @current_bytes + bytes > @max_bytes * 3
|
|
21
36
|
@dropped_count += 1
|
|
22
37
|
Catpm.stats[:dropped_events] += 1
|
|
23
38
|
return :dropped
|
|
@@ -25,8 +40,13 @@ module Catpm
|
|
|
25
40
|
|
|
26
41
|
@events << event
|
|
27
42
|
@current_bytes += bytes
|
|
28
|
-
|
|
43
|
+
|
|
44
|
+
signal_flush = @current_bytes >= @max_bytes
|
|
29
45
|
end
|
|
46
|
+
|
|
47
|
+
# Signal outside monitor to avoid holding the lock during callback
|
|
48
|
+
@flush_callback&.call if signal_flush
|
|
49
|
+
:accepted
|
|
30
50
|
end
|
|
31
51
|
|
|
32
52
|
# Called from flusher thread. Atomically swaps out the entire buffer.
|
data/lib/catpm/call_tracer.rb
CHANGED
|
@@ -2,11 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module Catpm
|
|
4
4
|
class CallTracer
|
|
5
|
+
MAX_CALL_DEPTH = 64
|
|
6
|
+
|
|
7
|
+
# Global thread-safe path classification cache — avoids repeated Fingerprint.app_frame? calls
|
|
8
|
+
@global_path_cache = {}
|
|
9
|
+
@global_path_mutex = Mutex.new
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def app_frame_cached?(path)
|
|
13
|
+
cached = @global_path_cache[path]
|
|
14
|
+
return cached unless cached.nil?
|
|
15
|
+
|
|
16
|
+
result = Fingerprint.app_frame?(path)
|
|
17
|
+
@global_path_mutex.synchronize do
|
|
18
|
+
# Cap cache to prevent unbounded growth across process lifetime
|
|
19
|
+
@global_path_cache.clear if @global_path_cache.size > 2000
|
|
20
|
+
@global_path_cache[path] = result
|
|
21
|
+
end
|
|
22
|
+
result
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
5
26
|
def initialize(request_segments:)
|
|
6
27
|
@request_segments = request_segments
|
|
7
28
|
@call_stack = []
|
|
8
|
-
@path_cache = {}
|
|
9
29
|
@started = false
|
|
30
|
+
@depth = 0
|
|
10
31
|
|
|
11
32
|
@tracepoint = TracePoint.new(:call, :return) do |tp|
|
|
12
33
|
case tp.event
|
|
@@ -36,14 +57,22 @@ module Catpm
|
|
|
36
57
|
private
|
|
37
58
|
|
|
38
59
|
def handle_call(tp)
|
|
60
|
+
@depth += 1
|
|
61
|
+
|
|
39
62
|
path = tp.path
|
|
40
|
-
app =
|
|
63
|
+
app = self.class.app_frame_cached?(path)
|
|
41
64
|
|
|
42
65
|
unless app
|
|
43
66
|
@call_stack.push(:skip)
|
|
44
67
|
return
|
|
45
68
|
end
|
|
46
69
|
|
|
70
|
+
# Prevent excessive nesting from blowing up memory
|
|
71
|
+
if @depth > MAX_CALL_DEPTH
|
|
72
|
+
@call_stack.push(:skip)
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
47
76
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
77
|
detail = format_detail(tp.defined_class, tp.method_id)
|
|
49
78
|
index = @request_segments.push_span(type: :code, detail: detail, started_at: started_at)
|
|
@@ -51,6 +80,7 @@ module Catpm
|
|
|
51
80
|
end
|
|
52
81
|
|
|
53
82
|
def handle_return
|
|
83
|
+
@depth -= 1 if @depth > 0
|
|
54
84
|
entry = @call_stack.pop
|
|
55
85
|
return if entry == :skip || entry.nil?
|
|
56
86
|
|
|
@@ -66,13 +96,6 @@ module Catpm
|
|
|
66
96
|
@call_stack.clear
|
|
67
97
|
end
|
|
68
98
|
|
|
69
|
-
def app_frame?(path)
|
|
70
|
-
cached = @path_cache[path]
|
|
71
|
-
return cached unless cached.nil?
|
|
72
|
-
|
|
73
|
-
@path_cache[path] = Fingerprint.app_frame?(path)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
99
|
def format_detail(defined_class, method_id)
|
|
77
100
|
if defined_class.singleton_class?
|
|
78
101
|
owner = defined_class.attached_object
|