go-duck-cli 1.4.8 → 1.4.12
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.
- package/generators/postman.js +5 -3
- package/generators/swagger.js +4 -2
- package/generators/telemetry.js +133 -6
- package/go.mod +15 -0
- package/go.sum +20 -0
- package/index.js +13 -4
- package/package.json +1 -1
- package/parser/gdl.js +9 -5
- package/templates/docs/pages/gdl-entities.hbs +53 -0
- package/templates/go/entity.go.hbs +3 -0
- package/templates/go/router.go.hbs +470 -44
- package/test-go/devops/k8s/otel-collector.yml +23 -0
- package/test-go/go.mod +64 -0
- package/test-go/go.sum +154 -0
- package/test-go/internal/telemetry/metrics_collector.go +88 -0
- package/test-go/internal/telemetry/otel.go +69 -0
- package/test-go/internal/telemetry/system_metrics.go +198 -0
- package/test_datatypes.go +19 -0
- package/test_time.go +22 -0
|
@@ -40,6 +40,227 @@ import (
|
|
|
40
40
|
"{{app_name}}/internal/search"
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
+
const compactWidgetHTML = `<!DOCTYPE html>
|
|
44
|
+
<html lang="en">
|
|
45
|
+
<head>
|
|
46
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
47
|
+
<title>Live Telemetry</title>
|
|
48
|
+
<style>
|
|
49
|
+
html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #121212; color: #fff; font-family: system-ui, sans-serif; overflow: hidden; }
|
|
50
|
+
.bento-grid { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; gap: 8px; width: 100%; height: 100%; padding: 8px; box-sizing: border-box; }
|
|
51
|
+
.bento-card { background: #1e1e1e; border-radius: 12px; display: flex; flex-direction: column; justify-content: center; align-items: center; position: relative; border: 1px solid #333; transition: all 0.2s; }
|
|
52
|
+
.bento-title { position: absolute; top: 10px; left: 10px; font-size: 0.75rem; color: #888; text-transform: uppercase; letter-spacing: 1px; }
|
|
53
|
+
.bento-badge { position: absolute; top: 10px; right: 10px; font-size: 0.6rem; padding: 2px 6px; border-radius: 4px; background: #e74c3c; color: #fff; display: none; }
|
|
54
|
+
.bento-value { font-size: 2.5rem; font-weight: 700; color: #fff; margin-top: 15px; display: flex; align-items: baseline; gap: 4px; }
|
|
55
|
+
.bento-unit { font-size: 1rem; color: #888; font-weight: normal; }
|
|
56
|
+
.bento-sub { font-size: 0.75rem; color: #666; margin-top: 5px; }
|
|
57
|
+
|
|
58
|
+
@keyframes alarmFlash {
|
|
59
|
+
0% { background: #1e1e1e; border-color: #333; }
|
|
60
|
+
50% { background: rgba(231, 76, 60, 0.2); border-color: #e74c3c; box-shadow: 0 0 15px rgba(231, 76, 60, 0.4); }
|
|
61
|
+
100% { background: #1e1e1e; border-color: #333; }
|
|
62
|
+
}
|
|
63
|
+
.alarm-active { animation: alarmFlash 1s infinite; }
|
|
64
|
+
</style>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<div class="bento-grid">
|
|
68
|
+
<div class="bento-card" id="card-cpu">
|
|
69
|
+
<div class="bento-title" id="cpu-title">CPU</div>
|
|
70
|
+
<div class="bento-badge" id="cpu-badge">QUOTA</div>
|
|
71
|
+
<div class="bento-value"><span id="cpu-val">0</span><span class="bento-unit">%</span></div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="bento-card" id="card-mem">
|
|
74
|
+
<div class="bento-title">RAM</div>
|
|
75
|
+
<div class="bento-value"><span id="mem-val">0</span><span class="bento-unit">%</span></div>
|
|
76
|
+
<div class="bento-sub"><span id="mem-raw">0 / 0 MB</span></div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<script>
|
|
80
|
+
// Apply custom background color if provided by Mission Control grid
|
|
81
|
+
const bg = new URLSearchParams(window.location.search).get('bg');
|
|
82
|
+
if (bg) {
|
|
83
|
+
document.body.style.background = bg;
|
|
84
|
+
document.querySelectorAll('.bento-card').forEach(c => {
|
|
85
|
+
c.style.background = 'transparent';
|
|
86
|
+
c.style.borderColor = 'rgba(255,255,255,0.1)';
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const evtSource = new EventSource('/api/system/stream');
|
|
91
|
+
evtSource.addEventListener('metrics', function(e) {
|
|
92
|
+
const data = JSON.parse(e.data);
|
|
93
|
+
const sys = data.system;
|
|
94
|
+
|
|
95
|
+
// CPU
|
|
96
|
+
let cpuPct = 0;
|
|
97
|
+
if (sys.is_pod_cpu_limited) {
|
|
98
|
+
document.getElementById('cpu-title').innerText = 'CPU';
|
|
99
|
+
document.getElementById('cpu-badge').style.display = 'block';
|
|
100
|
+
cpuPct = sys.pod_cpu_limit_pct;
|
|
101
|
+
} else {
|
|
102
|
+
document.getElementById('cpu-title').innerText = 'PROCESS CPU';
|
|
103
|
+
document.getElementById('cpu-badge').style.display = 'none';
|
|
104
|
+
cpuPct = sys.process_cpu_usage;
|
|
105
|
+
}
|
|
106
|
+
document.getElementById('cpu-val').innerText = cpuPct.toFixed(1);
|
|
107
|
+
|
|
108
|
+
if (cpuPct > 90) document.getElementById('card-cpu').classList.add('alarm-active');
|
|
109
|
+
else document.getElementById('card-cpu').classList.remove('alarm-active');
|
|
110
|
+
|
|
111
|
+
// Memory
|
|
112
|
+
let memPct = 0;
|
|
113
|
+
if (sys.total_mem_mb > 0) {
|
|
114
|
+
memPct = (sys.heap_alloc_mb / sys.total_mem_mb) * 100;
|
|
115
|
+
}
|
|
116
|
+
document.getElementById('mem-val').innerText = memPct.toFixed(1);
|
|
117
|
+
document.getElementById('mem-raw').innerText = sys.heap_alloc_mb + " / " + sys.total_mem_mb + " MB";
|
|
118
|
+
|
|
119
|
+
if (memPct > 90) document.getElementById('card-mem').classList.add('alarm-active');
|
|
120
|
+
else document.getElementById('card-mem').classList.remove('alarm-active');
|
|
121
|
+
});
|
|
122
|
+
evtSource.onerror = function() {
|
|
123
|
+
document.getElementById('cpu-val').innerText = "OFF";
|
|
124
|
+
document.getElementById('mem-val').innerText = "OFF";
|
|
125
|
+
document.getElementById('mem-raw').innerText = "Disconnected";
|
|
126
|
+
};
|
|
127
|
+
</script>
|
|
128
|
+
</body>
|
|
129
|
+
</html>`
|
|
130
|
+
|
|
131
|
+
const gridWidgetHTML = `<!DOCTYPE html>
|
|
132
|
+
<html lang="en">
|
|
133
|
+
<head>
|
|
134
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
135
|
+
<title>Mission Control Grid</title>
|
|
136
|
+
<style>
|
|
137
|
+
html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #000; font-family: system-ui, sans-serif; overflow: auto; }
|
|
138
|
+
.dashboard { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-auto-rows: minmax(30vh, 1fr); gap: 15px; padding: 15px; box-sizing: border-box; }
|
|
139
|
+
.grid-item { position: relative; width: 100%; height: 100%; background: #1a1a1a; border-radius: 12px; border: 1px solid #333; overflow: hidden; display: flex; flex-direction: column; transition: background 0.3s; }
|
|
140
|
+
.grid-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: rgba(0,0,0,0.4); border-bottom: 1px solid rgba(255,255,255,0.1); }
|
|
141
|
+
.grid-title { font-size: 0.85rem; font-weight: bold; color: #fff; text-transform: uppercase; letter-spacing: 1px; }
|
|
142
|
+
.grid-actions { display: flex; gap: 8px; align-items: center; }
|
|
143
|
+
.color-picker { width: 20px; height: 20px; padding: 0; border: none; border-radius: 4px; cursor: pointer; background: transparent; }
|
|
144
|
+
.close-btn { background: transparent; color: #888; border: none; font-size: 1.2rem; cursor: pointer; line-height: 1; transition: color 0.2s; }
|
|
145
|
+
.close-btn:hover { color: #e74c3c; }
|
|
146
|
+
.grid-item iframe { width: 100%; flex: 1; border: none; }
|
|
147
|
+
|
|
148
|
+
.add-item { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; transition: background 0.2s; color: #888; border: 2px dashed #444; border-radius: 12px; }
|
|
149
|
+
.add-item:hover { background: #222; color: #fff; border-color: #666; }
|
|
150
|
+
.add-icon { font-size: 3rem; font-weight: 300; margin-bottom: 10px; }
|
|
151
|
+
</style>
|
|
152
|
+
</head>
|
|
153
|
+
<body>
|
|
154
|
+
<div class="dashboard" id="dashboard">
|
|
155
|
+
<div class="add-item" id="add-btn" onclick="promptAddService()">
|
|
156
|
+
<div class="add-icon">+</div>
|
|
157
|
+
<div>Add Service</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<script>
|
|
161
|
+
let gridState = JSON.parse(localStorage.getItem('goduck_mission_control')) || [];
|
|
162
|
+
|
|
163
|
+
function saveState() {
|
|
164
|
+
localStorage.setItem('goduck_mission_control', JSON.stringify(gridState));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderGrid() {
|
|
168
|
+
// Clear all grid items except add button
|
|
169
|
+
document.querySelectorAll('.grid-item').forEach(el => el.remove());
|
|
170
|
+
|
|
171
|
+
// Add default widget if state is completely empty
|
|
172
|
+
if (gridState.length === 0) {
|
|
173
|
+
gridState.push({ id: 'default', name: '{{app_name}} (Host)', url: '/api/system/widget', color: '#1a1a1a' });
|
|
174
|
+
saveState();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const dash = document.getElementById('dashboard');
|
|
178
|
+
const addBtn = document.getElementById('add-btn');
|
|
179
|
+
|
|
180
|
+
gridState.forEach(widget => {
|
|
181
|
+
const wrapper = document.createElement('div');
|
|
182
|
+
wrapper.className = 'grid-item';
|
|
183
|
+
wrapper.style.backgroundColor = widget.color;
|
|
184
|
+
|
|
185
|
+
const header = document.createElement('div');
|
|
186
|
+
header.className = 'grid-header';
|
|
187
|
+
|
|
188
|
+
const title = document.createElement('div');
|
|
189
|
+
title.className = 'grid-title';
|
|
190
|
+
title.innerText = widget.name;
|
|
191
|
+
|
|
192
|
+
const actions = document.createElement('div');
|
|
193
|
+
actions.className = 'grid-actions';
|
|
194
|
+
|
|
195
|
+
const colorPicker = document.createElement('input');
|
|
196
|
+
colorPicker.type = 'color';
|
|
197
|
+
colorPicker.className = 'color-picker';
|
|
198
|
+
colorPicker.value = widget.color || '#1a1a1a';
|
|
199
|
+
colorPicker.title = 'Change Widget Color';
|
|
200
|
+
colorPicker.onchange = (e) => {
|
|
201
|
+
widget.color = e.target.value;
|
|
202
|
+
saveState();
|
|
203
|
+
renderGrid(); // Re-render to pass new color to iframe
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const closeBtn = document.createElement('button');
|
|
207
|
+
closeBtn.className = 'close-btn';
|
|
208
|
+
closeBtn.innerHTML = '×';
|
|
209
|
+
closeBtn.title = 'Remove Widget';
|
|
210
|
+
closeBtn.onclick = () => {
|
|
211
|
+
if(confirm("Remove " + widget.name + " from Mission Control?")) {
|
|
212
|
+
gridState = gridState.filter(w => w.id !== widget.id);
|
|
213
|
+
saveState();
|
|
214
|
+
renderGrid();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
actions.appendChild(colorPicker);
|
|
219
|
+
actions.appendChild(closeBtn);
|
|
220
|
+
header.appendChild(title);
|
|
221
|
+
header.appendChild(actions);
|
|
222
|
+
|
|
223
|
+
const iframe = document.createElement('iframe');
|
|
224
|
+
// Append color parameter to URL to theme the iframe natively
|
|
225
|
+
try {
|
|
226
|
+
const urlObj = new URL(widget.url, window.location.origin);
|
|
227
|
+
urlObj.searchParams.set('bg', widget.color);
|
|
228
|
+
iframe.src = urlObj.toString();
|
|
229
|
+
} catch (e) {
|
|
230
|
+
iframe.src = widget.url; // Fallback if URL is invalid
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
wrapper.appendChild(header);
|
|
234
|
+
wrapper.appendChild(iframe);
|
|
235
|
+
|
|
236
|
+
dash.insertBefore(wrapper, addBtn);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function promptAddService() {
|
|
241
|
+
const name = prompt("Enter a Name for this Service (e.g., 'Auth Service'):");
|
|
242
|
+
if (!name) return;
|
|
243
|
+
|
|
244
|
+
const url = prompt("Enter the remote widget URL:\\n(e.g., http://localhost:8081/api/system/widget)");
|
|
245
|
+
if (!url) return;
|
|
246
|
+
|
|
247
|
+
gridState.push({
|
|
248
|
+
id: Date.now().toString(),
|
|
249
|
+
name: name,
|
|
250
|
+
url: url,
|
|
251
|
+
color: '#1a1a1a'
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
saveState();
|
|
255
|
+
renderGrid();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Initial render
|
|
259
|
+
renderGrid();
|
|
260
|
+
</script>
|
|
261
|
+
</body>
|
|
262
|
+
</html>`
|
|
263
|
+
|
|
43
264
|
func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
44
265
|
// 1. Initialize Logging & Observability
|
|
45
266
|
logger.InitLogger(appConfig)
|
|
@@ -232,45 +453,39 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
232
453
|
c.Header("Connection", "keep-alive")
|
|
233
454
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
234
455
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
interval = time.Second
|
|
238
|
-
}
|
|
456
|
+
clientChan := make(chan telemetry.AppMetrics, 100)
|
|
457
|
+
telemetry.GlobalMetricsBroker.AddClient(clientChan)
|
|
239
458
|
|
|
240
|
-
|
|
241
|
-
|
|
459
|
+
defer func() {
|
|
460
|
+
telemetry.GlobalMetricsBroker.RemoveClient(clientChan)
|
|
461
|
+
}()
|
|
242
462
|
|
|
243
463
|
c.Stream(func(w io.Writer) bool {
|
|
244
464
|
select {
|
|
245
465
|
case <-c.Request.Context().Done():
|
|
246
466
|
return false
|
|
247
|
-
case <-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Simple load calculation
|
|
257
|
-
loadPct := (cpuUsage + memStats.UsedPercent) / 2.0
|
|
258
|
-
|
|
259
|
-
payload := map[string]interface{}{
|
|
260
|
-
"timestamp": time.Now().Format(time.RFC3339),
|
|
261
|
-
"cpu_percent": cpuUsage,
|
|
262
|
-
"mem_percent": memStats.UsedPercent,
|
|
263
|
-
"mem_used_mb": memStats.Used / 1024 / 1024,
|
|
264
|
-
"mem_total_mb": memStats.Total / 1024 / 1024,
|
|
265
|
-
"load_percentage": loadPct,
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
data, _ := json.Marshal(payload)
|
|
269
|
-
c.SSEvent("message", string(data))
|
|
467
|
+
case appMetrics := <-clientChan:
|
|
468
|
+
sysMetrics := telemetry.CollectSystemMetrics()
|
|
469
|
+
c.SSEvent("metrics", gin.H{
|
|
470
|
+
"system": sysMetrics,
|
|
471
|
+
"endpoints": appMetrics.Endpoints,
|
|
472
|
+
"status_codes": appMetrics.StatusCodes,
|
|
473
|
+
"failed_calls": appMetrics.FailedCalls,
|
|
474
|
+
})
|
|
270
475
|
return true
|
|
271
476
|
}
|
|
272
477
|
})
|
|
273
478
|
})
|
|
479
|
+
|
|
480
|
+
// Compact Bento Telemetry Widget
|
|
481
|
+
r.GET("/api/system/widget", func(c *gin.Context) {
|
|
482
|
+
c.Data(200, "text/html; charset=utf-8", []byte(compactWidgetHTML))
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// Mission Control Iframe Grid
|
|
486
|
+
r.GET("/api/system/grid", func(c *gin.Context) {
|
|
487
|
+
c.Data(200, "text/html; charset=utf-8", []byte(gridWidgetHTML))
|
|
488
|
+
})
|
|
274
489
|
}
|
|
275
490
|
|
|
276
491
|
// Swagger Docs & UI
|
|
@@ -293,6 +508,15 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
293
508
|
*, *:before, *:after { box-sizing: inherit; }
|
|
294
509
|
body { margin:0; background: #fafafa; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
|
295
510
|
|
|
511
|
+
.nav-trigger-zone {
|
|
512
|
+
position: fixed;
|
|
513
|
+
top: 0;
|
|
514
|
+
left: 0;
|
|
515
|
+
right: 0;
|
|
516
|
+
height: 30px;
|
|
517
|
+
z-index: 999;
|
|
518
|
+
}
|
|
519
|
+
|
|
296
520
|
.top-nav {
|
|
297
521
|
background: rgba(255, 255, 255, 0.8);
|
|
298
522
|
backdrop-filter: blur(10px);
|
|
@@ -302,11 +526,20 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
302
526
|
display: flex;
|
|
303
527
|
justify-content: space-between;
|
|
304
528
|
align-items: center;
|
|
305
|
-
position:
|
|
306
|
-
|
|
529
|
+
position: fixed;
|
|
530
|
+
width: 100%;
|
|
531
|
+
top: -100px;
|
|
532
|
+
opacity: 0;
|
|
533
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
307
534
|
z-index: 1000;
|
|
308
535
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
|
|
309
536
|
}
|
|
537
|
+
|
|
538
|
+
.nav-trigger-zone:hover + .top-nav,
|
|
539
|
+
.top-nav:hover {
|
|
540
|
+
top: 0;
|
|
541
|
+
opacity: 1;
|
|
542
|
+
}
|
|
310
543
|
|
|
311
544
|
.nav-brand {
|
|
312
545
|
font-weight: 700;
|
|
@@ -446,6 +679,14 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
446
679
|
}
|
|
447
680
|
|
|
448
681
|
/* System Metrics Dashboard */
|
|
682
|
+
@keyframes flashRedOverlay {
|
|
683
|
+
0% { background: rgba(0,0,0,0.8); }
|
|
684
|
+
50% { background: rgba(255,0,0,0.5); }
|
|
685
|
+
100% { background: rgba(0,0,0,0.8); }
|
|
686
|
+
}
|
|
687
|
+
.alarm-flash-screen {
|
|
688
|
+
animation: flashRedOverlay 1s infinite !important;
|
|
689
|
+
}
|
|
449
690
|
#sys-metrics-modal {
|
|
450
691
|
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
451
692
|
background: rgba(0,0,0,0.8); z-index: 4000; justify-content: center; align-items: center;
|
|
@@ -468,13 +709,47 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
468
709
|
.metrics-tabs { display: flex; gap: 10px; margin-bottom: 20px; }
|
|
469
710
|
.metrics-tab {
|
|
470
711
|
padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer;
|
|
471
|
-
font-weight: 600; background: #eee; color: #555;
|
|
712
|
+
font-weight: 600; background: #eee; color: #555;
|
|
472
713
|
}
|
|
714
|
+
.metrics-tab:hover { background: #e0e0e0; }
|
|
473
715
|
.metrics-tab.active { background: #4a90e2; color: white; }
|
|
474
|
-
|
|
716
|
+
|
|
717
|
+
.cpu-threads-grid {
|
|
718
|
+
display: grid;
|
|
719
|
+
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
|
720
|
+
gap: 10px;
|
|
721
|
+
margin-top: 15px;
|
|
722
|
+
}
|
|
723
|
+
.cpu-chart-container {
|
|
724
|
+
border: 1px solid #7eb4ea;
|
|
725
|
+
background: #f7fbff;
|
|
726
|
+
display: flex;
|
|
727
|
+
flex-direction: column;
|
|
728
|
+
}
|
|
729
|
+
.cpu-chart-canvas {
|
|
730
|
+
width: 100%;
|
|
731
|
+
height: 50px;
|
|
732
|
+
display: block;
|
|
733
|
+
border-bottom: 1px solid #7eb4ea;
|
|
734
|
+
}
|
|
735
|
+
.cpu-chart-label {
|
|
736
|
+
text-align: center;
|
|
737
|
+
font-size: 0.65rem;
|
|
738
|
+
color: #333;
|
|
739
|
+
padding: 3px 0;
|
|
740
|
+
background: #eef5fb;
|
|
741
|
+
}
|
|
742
|
+
|
|
475
743
|
.metrics-dashboard-grid {
|
|
476
|
-
display: grid;
|
|
477
|
-
|
|
744
|
+
display: grid;
|
|
745
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
746
|
+
gap: 20px;
|
|
747
|
+
overflow-y: auto;
|
|
748
|
+
padding-right: 10px;
|
|
749
|
+
}
|
|
750
|
+
@media (max-width: 600px) {
|
|
751
|
+
.metrics-tabs { flex-wrap: wrap; }
|
|
752
|
+
.metrics-tab { flex: 1 1 40%; text-align: center; }
|
|
478
753
|
}
|
|
479
754
|
.metric-card {
|
|
480
755
|
background: white; border-radius: 8px; padding: 15px;
|
|
@@ -537,7 +812,8 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
537
812
|
.log-line.hidden { display: none; }
|
|
538
813
|
</style>
|
|
539
814
|
</head>
|
|
540
|
-
<body>
|
|
815
|
+
<body id="swagger-body">
|
|
816
|
+
<div class="nav-trigger-zone"></div>
|
|
541
817
|
<div class="top-nav">
|
|
542
818
|
<div class="nav-brand">
|
|
543
819
|
<img src="/logo.png" alt="GO-DUCK Logo" style="height: 32px; width: auto;" />
|
|
@@ -558,6 +834,7 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
558
834
|
<button id="logout-btn" class="btn btn-logout">Logout</button>
|
|
559
835
|
<button id="mqtt-btn" class="btn" style="background: #2c3e50; color: white;">MQTT Topics</button>
|
|
560
836
|
<button id="sys-metrics-btn" class="btn" style="background: #8e44ad; color: white;">System Metrics</button>
|
|
837
|
+
<button id="compact-widget-btn" class="btn" style="background: #27ae60; color: white; margin-right: 5px;" title="Open Mission Control Grid">Mission Control</button>
|
|
561
838
|
<button id="live-logs-btn" class="btn" style="background: #d35400; color: white;" title="Stream Server Console Logs">Live Logs</button>
|
|
562
839
|
</div>
|
|
563
840
|
</div>
|
|
@@ -616,13 +893,20 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
616
893
|
<div id="metrics-dashboard-view" class="metrics-dashboard-grid">
|
|
617
894
|
<!-- CPU Card -->
|
|
618
895
|
<div class="metric-card">
|
|
619
|
-
<div class="metric-title"
|
|
896
|
+
<div class="metric-title" style="display:flex; justify-content: space-between;">
|
|
897
|
+
<span id="m-cpu-title">Process CPU</span>
|
|
898
|
+
<span id="m-cpu-badge" style="font-size: 0.8rem; background:#e74c3c; color:white; padding:2px 6px; border-radius:4px; display:none;">Pod Quota</span>
|
|
899
|
+
</div>
|
|
620
900
|
<div class="metric-value"><span id="m-cpu">0.00</span><span class="metric-unit">%</span></div>
|
|
621
901
|
<div class="progress-bar-container"><div id="p-cpu" class="progress-bar" style="width: 0%"></div></div>
|
|
902
|
+
<div id="cpu-threads-grid" class="cpu-threads-grid"></div>
|
|
622
903
|
</div>
|
|
623
904
|
<!-- Memory Card -->
|
|
624
905
|
<div class="metric-card">
|
|
625
|
-
<div class="metric-title"
|
|
906
|
+
<div class="metric-title" style="display:flex; justify-content: space-between;">
|
|
907
|
+
<span>Heap Alloc</span>
|
|
908
|
+
<span style="font-size: 0.8rem; color: #888; font-weight: normal;">OS: <span id="m-sys">0</span> MB</span>
|
|
909
|
+
</div>
|
|
626
910
|
<div class="metric-value"><span id="m-heap">0</span><span class="metric-unit">MB</span></div>
|
|
627
911
|
<div class="progress-bar-container"><div id="p-heap" class="progress-bar" style="width: 0%"></div></div>
|
|
628
912
|
</div>
|
|
@@ -847,6 +1131,25 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
847
1131
|
// Metrics Dashboard Logic
|
|
848
1132
|
const sysMetricsModal = document.getElementById('sys-metrics-modal');
|
|
849
1133
|
let metricsInterval = null;
|
|
1134
|
+
let cpuThreadHistory = {};
|
|
1135
|
+
const MAX_HISTORY_POINTS = 20;
|
|
1136
|
+
|
|
1137
|
+
// Audio Alarm Logic
|
|
1138
|
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
1139
|
+
function playAlarm() {
|
|
1140
|
+
if(audioCtx.state === 'suspended') audioCtx.resume();
|
|
1141
|
+
const oscillator = audioCtx.createOscillator();
|
|
1142
|
+
const gainNode = audioCtx.createGain();
|
|
1143
|
+
oscillator.type = 'square';
|
|
1144
|
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime); // 880 Hz
|
|
1145
|
+
oscillator.frequency.exponentialRampToValueAtTime(440, audioCtx.currentTime + 0.3);
|
|
1146
|
+
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
|
1147
|
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
|
|
1148
|
+
oscillator.connect(gainNode);
|
|
1149
|
+
gainNode.connect(audioCtx.destination);
|
|
1150
|
+
oscillator.start();
|
|
1151
|
+
oscillator.stop(audioCtx.currentTime + 0.3);
|
|
1152
|
+
}
|
|
850
1153
|
|
|
851
1154
|
function syntaxHighlight(json) {
|
|
852
1155
|
if (typeof json != 'string') {
|
|
@@ -896,11 +1199,108 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
896
1199
|
.then(data => {
|
|
897
1200
|
// Update Dashboard UI
|
|
898
1201
|
const sys = data.system;
|
|
899
|
-
|
|
900
|
-
|
|
1202
|
+
|
|
1203
|
+
if (sys.is_pod_cpu_limited) {
|
|
1204
|
+
document.getElementById('m-cpu-title').innerText = "Pod CPU Quota";
|
|
1205
|
+
document.getElementById('m-cpu-badge').style.display = 'inline-block';
|
|
1206
|
+
document.getElementById('m-cpu').innerText = sys.pod_cpu_limit_pct.toFixed(2);
|
|
1207
|
+
updateProgressBar('p-cpu', sys.pod_cpu_limit_pct, 100);
|
|
1208
|
+
} else {
|
|
1209
|
+
document.getElementById('m-cpu-title').innerText = "Process CPU";
|
|
1210
|
+
document.getElementById('m-cpu-badge').style.display = 'none';
|
|
1211
|
+
document.getElementById('m-cpu').innerText = sys.process_cpu_usage.toFixed(2);
|
|
1212
|
+
updateProgressBar('p-cpu', sys.process_cpu_usage, 100);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const threadsGrid = document.getElementById('cpu-threads-grid');
|
|
1216
|
+
if (threadsGrid && sys.cpu_threads_usage) {
|
|
1217
|
+
if (threadsGrid.children.length !== sys.cpu_threads_usage.length) {
|
|
1218
|
+
let html = '';
|
|
1219
|
+
sys.cpu_threads_usage.forEach((val, idx) => {
|
|
1220
|
+
html += `<div class="cpu-chart-container" title="Core ${idx}">
|
|
1221
|
+
<canvas id="t-cpu-canvas-${idx}" class="cpu-chart-canvas"></canvas>
|
|
1222
|
+
<div id="t-cpu-label-${idx}" class="cpu-chart-label">T${idx}: 0%</div>
|
|
1223
|
+
</div>`;
|
|
1224
|
+
});
|
|
1225
|
+
threadsGrid.innerHTML = html;
|
|
1226
|
+
|
|
1227
|
+
// Set internal canvas resolution to match computed CSS size for crisp rendering
|
|
1228
|
+
sys.cpu_threads_usage.forEach((val, idx) => {
|
|
1229
|
+
const canvas = document.getElementById(`t-cpu-canvas-${idx}`);
|
|
1230
|
+
if (canvas) {
|
|
1231
|
+
canvas.width = canvas.offsetWidth;
|
|
1232
|
+
canvas.height = canvas.offsetHeight;
|
|
1233
|
+
cpuThreadHistory[idx] = new Array(MAX_HISTORY_POINTS).fill(0);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
sys.cpu_threads_usage.forEach((val, idx) => {
|
|
1239
|
+
// Update History Array
|
|
1240
|
+
if (!cpuThreadHistory[idx]) {
|
|
1241
|
+
cpuThreadHistory[idx] = new Array(MAX_HISTORY_POINTS).fill(0);
|
|
1242
|
+
}
|
|
1243
|
+
cpuThreadHistory[idx].push(val);
|
|
1244
|
+
if (cpuThreadHistory[idx].length > MAX_HISTORY_POINTS) {
|
|
1245
|
+
cpuThreadHistory[idx].shift();
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Render Text Label
|
|
1249
|
+
const label = document.getElementById(`t-cpu-label-${idx}`);
|
|
1250
|
+
if (label) {
|
|
1251
|
+
label.innerText = `T${idx}: ${val.toFixed(0)}%`;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Render Canvas Sparkline
|
|
1255
|
+
const canvas = document.getElementById(`t-cpu-canvas-${idx}`);
|
|
1256
|
+
if (canvas) {
|
|
1257
|
+
const ctx = canvas.getContext('2d');
|
|
1258
|
+
const width = canvas.width;
|
|
1259
|
+
const height = canvas.height;
|
|
1260
|
+
const history = cpuThreadHistory[idx];
|
|
1261
|
+
|
|
1262
|
+
ctx.clearRect(0, 0, width, height);
|
|
1263
|
+
|
|
1264
|
+
// Draw light grid lines like Task Manager
|
|
1265
|
+
ctx.strokeStyle = '#cce3f9';
|
|
1266
|
+
ctx.lineWidth = 1;
|
|
1267
|
+
ctx.beginPath();
|
|
1268
|
+
for(let i=1; i<4; i++) {
|
|
1269
|
+
ctx.moveTo(0, height * (i/4));
|
|
1270
|
+
ctx.lineTo(width, height * (i/4));
|
|
1271
|
+
}
|
|
1272
|
+
for(let i=1; i<5; i++) {
|
|
1273
|
+
ctx.moveTo(width * (i/5), 0);
|
|
1274
|
+
ctx.lineTo(width * (i/5), height);
|
|
1275
|
+
}
|
|
1276
|
+
ctx.stroke();
|
|
1277
|
+
|
|
1278
|
+
// Draw Data Line
|
|
1279
|
+
ctx.beginPath();
|
|
1280
|
+
ctx.strokeStyle = '#1a73e8';
|
|
1281
|
+
ctx.lineWidth = 1.5;
|
|
1282
|
+
|
|
1283
|
+
const step = width / (MAX_HISTORY_POINTS - 1);
|
|
1284
|
+
history.forEach((point, pIdx) => {
|
|
1285
|
+
const x = pIdx * step;
|
|
1286
|
+
const y = height - (point / 100) * height;
|
|
1287
|
+
if (pIdx === 0) ctx.moveTo(x, y);
|
|
1288
|
+
else ctx.lineTo(x, y);
|
|
1289
|
+
});
|
|
1290
|
+
ctx.stroke();
|
|
1291
|
+
|
|
1292
|
+
// Draw Fill Underneath
|
|
1293
|
+
ctx.lineTo(width, height);
|
|
1294
|
+
ctx.lineTo(0, height);
|
|
1295
|
+
ctx.fillStyle = 'rgba(26, 115, 232, 0.2)';
|
|
1296
|
+
ctx.fill();
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
901
1300
|
|
|
902
1301
|
document.getElementById('m-heap').innerText = sys.heap_alloc_mb;
|
|
903
|
-
|
|
1302
|
+
document.getElementById('m-sys').innerText = sys.total_mem_mb;
|
|
1303
|
+
updateProgressBar('p-heap', sys.heap_alloc_mb, sys.total_mem_mb > 0 ? sys.total_mem_mb : 1024);
|
|
904
1304
|
|
|
905
1305
|
document.getElementById('m-goroutines').innerText = sys.goroutines;
|
|
906
1306
|
document.getElementById('m-uptime').innerText = sys.uptime;
|
|
@@ -910,6 +1310,24 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
910
1310
|
document.getElementById('m-http-ok').innerText = data.status_codes['200'] ? data.status_codes['200'].count : 0;
|
|
911
1311
|
document.getElementById('m-http-fail').innerText = data.failed_calls;
|
|
912
1312
|
|
|
1313
|
+
// Alarm Detection (Ceiling > 90%)
|
|
1314
|
+
let isAlarmState = false;
|
|
1315
|
+
let currentCpuVal = sys.is_pod_cpu_limited ? sys.pod_cpu_limit_pct : sys.process_cpu_usage;
|
|
1316
|
+
if (currentCpuVal > 90) isAlarmState = true;
|
|
1317
|
+
if (sys.total_mem_mb > 0 && (sys.heap_alloc_mb / sys.total_mem_mb) > 0.90) isAlarmState = true;
|
|
1318
|
+
|
|
1319
|
+
const modalWrapper = document.getElementById('sys-metrics-modal');
|
|
1320
|
+
const modalContent = document.querySelector('.metrics-content');
|
|
1321
|
+
|
|
1322
|
+
if (isAlarmState) {
|
|
1323
|
+
playAlarm();
|
|
1324
|
+
if(modalWrapper) modalWrapper.classList.add('alarm-flash-screen');
|
|
1325
|
+
if(modalContent) modalContent.style.boxShadow = '0 0 50px rgba(255, 0, 0, 0.6)';
|
|
1326
|
+
} else {
|
|
1327
|
+
if(modalWrapper) modalWrapper.classList.remove('alarm-flash-screen');
|
|
1328
|
+
if(modalContent) modalContent.style.boxShadow = '0 10px 40px rgba(0,0,0,0.2)';
|
|
1329
|
+
}
|
|
1330
|
+
|
|
913
1331
|
// Endpoints Table
|
|
914
1332
|
const tbody = document.getElementById('m-endpoints-body');
|
|
915
1333
|
tbody.innerHTML = '';
|
|
@@ -1122,7 +1540,7 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
1122
1540
|
}
|
|
1123
1541
|
|
|
1124
1542
|
document.getElementById('live-logs-btn').onclick = () => {
|
|
1125
|
-
if (!keycloak.authenticated) {
|
|
1543
|
+
if (!keycloak.authenticated && {{appConfig.GoDuck.Security.OIDC.Enabled}}) {
|
|
1126
1544
|
alert('Please login to view Server Logs');
|
|
1127
1545
|
return;
|
|
1128
1546
|
}
|
|
@@ -1134,13 +1552,13 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
1134
1552
|
// Connect SSE
|
|
1135
1553
|
logsEventSource = new EventSource('/api/system/logs/stream');
|
|
1136
1554
|
|
|
1137
|
-
logsEventSource.
|
|
1555
|
+
logsEventSource.addEventListener('log', function(event) {
|
|
1138
1556
|
// Remove connecting message on first log
|
|
1139
1557
|
if (logBody.children.length === 1 && logBody.firstChild.innerText.includes('Connecting')) {
|
|
1140
1558
|
logBody.innerHTML = '';
|
|
1141
1559
|
}
|
|
1142
1560
|
appendLogLine(event.data);
|
|
1143
|
-
};
|
|
1561
|
+
});
|
|
1144
1562
|
|
|
1145
1563
|
logsEventSource.onerror = function() {
|
|
1146
1564
|
appendLogLine('[SSE Error] Disconnected from log stream.');
|
|
@@ -1148,6 +1566,14 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
1148
1566
|
};
|
|
1149
1567
|
};
|
|
1150
1568
|
|
|
1569
|
+
document.getElementById('compact-widget-btn').onclick = () => {
|
|
1570
|
+
if (!keycloak.authenticated && {{appConfig.GoDuck.Security.OIDC.Enabled}}) {
|
|
1571
|
+
alert('Please login to view system telemetry.');
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
window.open('/api/system/grid', 'MissionControlGrid', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no');
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1151
1577
|
document.getElementById('live-logs-close').onclick = () => {
|
|
1152
1578
|
liveLogsModal.style.display = 'none';
|
|
1153
1579
|
if (logsEventSource) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
receivers:
|
|
3
|
+
otlp:
|
|
4
|
+
protocols:
|
|
5
|
+
grpc:
|
|
6
|
+
http:
|
|
7
|
+
processors:
|
|
8
|
+
batch:
|
|
9
|
+
resourcedetection:
|
|
10
|
+
detectors: [env, system]
|
|
11
|
+
exporters:
|
|
12
|
+
logging:
|
|
13
|
+
loglevel: debug
|
|
14
|
+
otlp:
|
|
15
|
+
endpoint: "jaeger:4317"
|
|
16
|
+
tls:
|
|
17
|
+
insecure: true
|
|
18
|
+
service:
|
|
19
|
+
pipelines:
|
|
20
|
+
traces:
|
|
21
|
+
receivers: [otlp]
|
|
22
|
+
processors: [batch, resourcedetection]
|
|
23
|
+
exporters: [logging, otlp]
|