feat(debug): add Telemetria teszt section to debug page (v0.28.1)

- New GET /api/debug/telemetry endpoint runs full telemetry pipeline on-demand
- GetTelemetryPreview callback added to DebugCallbacks, wired in main.go
- BuildAppTelemetryForDebug() exported wrapper in report/telemetry.go
- Debug page: new collapsible section with per-app table (memory, CPU, log errors/warnings, issues) and raw JSON viewer
- Available regardless of hub configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 11:09:06 +01:00
parent 6d9937bdc1
commit 4a6ab4d61c
6 changed files with 161 additions and 3 deletions
+9
View File
@@ -1,5 +1,14 @@
## Changelog
### v0.28.1 — Telemetry Debug Section (2026-02-23)
#### Added
- **Telemetria teszt section on Debug page** — New collapsible section between "Hub & Kapcsolatok" and "Önfrissítés teszt". Click "Telemetria futtatása" to run the full telemetry collection pipeline on-demand without waiting for the 15-minute report cycle.
- **`GET /api/debug/telemetry`** — New debug endpoint in `handler_debug.go`. Invokes `GetTelemetryPreview` callback, returns per-app data: container list, memory (current/avg/peak), CPU avg, catalog limit, log error/warning counts, top issues, and overall latency. Response: `{latency_ms, app_count, total_errors, total_warnings, app_telemetry[]}`.
- **`GetTelemetryPreview` callback** added to `DebugCallbacks` struct. Wired in `main.go` debug-mode block: calls `report.BuildAppTelemetryForDebug(stackMgr, metricsStore, logger)`. Available regardless of hub configuration.
- **`report.BuildAppTelemetryForDebug()`** — Exported wrapper in `internal/report/telemetry.go` around the private `buildAppTelemetrySection()`. Allows debug endpoint access without exposing internal package details.
- **JS rendering** — `runTelemetryTest()` fetches the endpoint and shows a summary message. `renderTelemetryDetail()` builds a table with per-app rows (color-coded errors in red, warnings in yellow) and sub-rows for top issues. Includes a collapsible "Nyers JSON" section showing the exact payload that would go to the hub.
### v0.28.0 — App Telemetry & Analytics (2026-02-23)
#### Added
+6 -3
View File
@@ -4,7 +4,7 @@
A single, lightweight Go container that replaces Portainer + scattered systemd scripts with a unified, Hungarian-language web dashboard for managing Docker Compose stacks, backups, storage, monitoring, and notifications on customer hardware.
**Current version: v0.28.0**
**Current version: v0.28.1**
---
@@ -1038,7 +1038,7 @@ The Hub serves three asset types per app:
### 12. Debug Mode
When `logging.level: "debug"` is set in `controller.yaml`, the controller exposes a full diagnostic dashboard at `/debug` with 8 testing sections. All debug endpoints are gated — at `info` level, the sidebar link disappears and all `/api/debug/*` routes return 404.
When `logging.level: "debug"` is set in `controller.yaml`, the controller exposes a full diagnostic dashboard at `/debug` with 9 testing sections. All debug endpoints are gated — at `info` level, the sidebar link disappears and all `/api/debug/*` routes return 404.
#### Debug Page Sections
@@ -1049,6 +1049,7 @@ When `logging.level: "debug"` is set in `controller.yaml`, the controller expose
| 3 | Mentés teszt | `POST /api/debug/backup/{dbdump,crossdrive,integrity,infra}` | Trigger individual backup phases independently. |
| 4 | Tárhely teszt | `POST /api/debug/storage/simulate-{disconnect,reconnect}`, `GET /api/debug/storage/watchdog-status` | Simulate drive disconnect/reconnect without unmounting. Per-path probe state with 5s auto-refresh. |
| 5 | Hub & Kapcsolatok | `POST /api/debug/hub/{push,infra-push,test-connectivity,preferences-sync}`, `POST /api/debug/gitea/test-connectivity` | Test Hub/Gitea connectivity with latency. Push reports and sync preferences. |
| — | Telemetria teszt | `GET /api/debug/telemetry` | Run the full telemetry collection pipeline on-demand (metrics query + log scan). Returns per-app table: container list, memory current/avg/peak, CPU avg, catalog limit, log error/warning counts, and top issues. Useful for verifying container→stack mapping and testing log scanner patterns without waiting for the 15-minute report cycle. |
| 6 | Önfrissítés teszt | `POST /api/debug/selfupdate/dry-run` | Dry-run update check: current vs new image lines, compose writability, backup state. |
| 7 | DR / Telepítő varázsló | `POST /api/debug/dr/trigger-setup`, `GET /api/debug/dr/infra-status` | Infra backup status per drive. Trigger setup mode via marker file (requires "RESET" + infra backup pre-check). |
| 8 | Naplóviewer | `GET /api/debug/logs?level=&limit=&after=` | In-memory log viewer (last 1000 entries), level filter, 2s auto-refresh, color-coded entries. |
@@ -1059,7 +1060,8 @@ When `logging.level: "debug"` is set in `controller.yaml`, the controller expose
- **Storage simulation**: `simulatedPaths` map in watchdog prevents the watchdog from re-probing simulated-disconnected paths. Disconnect runs all real steps except `lazyUnmount` (drive stays physically mounted).
- **DR trigger safety**: Uses marker file (`data/.needs-setup`) instead of modifying controller.yaml. Pre-checks that infra backup exists on at least one drive.
- **Routing**: `/api/debug/` carved out in HTTP mux (same pattern as `/api/storage/`), routed to web server with auth + CSRF.
- **DebugCallbacks**: 6 closures wired from main.go for operations needing modules not on Server struct (hub push, infra backup, connectivity tests).
- **DebugCallbacks**: 7 closures wired from main.go for operations needing modules not on Server struct (hub push, infra backup, connectivity tests, telemetry preview).
- **Telemetry debug**: `GetTelemetryPreview` callback calls `report.BuildAppTelemetryForDebug()` (exported wrapper around the private `buildAppTelemetrySection()`). Result renders as a table with collapsible raw JSON. Available regardless of hub configuration.
---
@@ -1344,6 +1346,7 @@ Config endpoints accept session auth OR `Authorization: Bearer <hub_api_key>` (s
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/debug/dump` | Full diagnostic JSON dump (controller state, storage, stacks, backup, hub, scheduler, health, alerts). Returns 404 when `logging.level` is not `"debug"`. |
| GET | `/api/debug/telemetry` | Run telemetry collection on-demand; returns per-app metrics + log summary with latency. Response: `{latency_ms, app_count, total_errors, total_warnings, app_telemetry[]}`. |
Response format: `{"ok": true/false, "data": ..., "error": "...", "message": "..."}`
+3
View File
@@ -650,6 +650,9 @@ func main() {
return resp.StatusCode, latency, nil
}
}
dc.GetTelemetryPreview = func() ([]report.AppTelemetry, error) {
return report.BuildAppTelemetryForDebug(stackMgr, metricsStore, logger), nil
}
webServer.SetDebugCallbacks(dc)
}
+7
View File
@@ -117,3 +117,10 @@ func buildAppTelemetry(allStacks []stacks.Stack, telemetry []metrics.ContainerTe
}
return result
}
// BuildAppTelemetryForDebug runs the full telemetry collection pipeline
// (metrics query + log scan) and returns per-app telemetry data.
// Used by the debug endpoint to preview telemetry without pushing to hub.
func BuildAppTelemetryForDebug(stackMgr *stacks.Manager, metricsStore *metrics.MetricsStore, logger *log.Logger) []AppTelemetry {
return buildAppTelemetrySection(stackMgr, metricsStore, logger)
}
+40
View File
@@ -15,6 +15,7 @@ import (
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
"gitea.dooplex.hu/admin/felhom-controller/internal/report"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
)
@@ -27,6 +28,7 @@ type DebugCallbacks struct {
TriggerSetupMode func() error
HubConnectivityTest func() (statusCode int, latencyMs int64, err error)
GiteaConnectivityTest func() (statusCode int, latencyMs int64, err error)
GetTelemetryPreview func() ([]report.AppTelemetry, error)
}
// debugPageHandler renders the debug dashboard page.
@@ -80,6 +82,10 @@ func (s *Server) handleDebugAPI(w http.ResponseWriter, r *http.Request) {
case subpath == "gitea/test-connectivity" && r.Method == http.MethodPost:
s.debugGiteaConnectivity(w, r)
// Section: Telemetry testing
case subpath == "telemetry" && r.Method == http.MethodGet:
s.debugTelemetry(w, r)
// Section 6: Self-update
case subpath == "selfupdate/dry-run" && r.Method == http.MethodPost:
s.debugSelfUpdateDryRun(w, r)
@@ -538,6 +544,40 @@ func (s *Server) debugGiteaConnectivity(w http.ResponseWriter, r *http.Request)
fmt.Sprintf("Gitea elérhető (HTTP %d, %dms)", statusCode, latency), data)
}
// ── Section: Telemetry testing ───────────────────────────────────────
func (s *Server) debugTelemetry(w http.ResponseWriter, r *http.Request) {
if s.debugCallbacks == nil || s.debugCallbacks.GetTelemetryPreview == nil {
writeDebugJSON(w, http.StatusNotImplemented, false, "Nem bekötött", nil)
return
}
start := time.Now()
telemetry, err := s.debugCallbacks.GetTelemetryPreview()
latency := time.Since(start).Milliseconds()
if err != nil {
writeDebugJSON(w, http.StatusOK, false, err.Error(), map[string]interface{}{"latency_ms": latency})
return
}
totalErrors := 0
totalWarnings := 0
for _, app := range telemetry {
totalErrors += app.LogErrors
totalWarnings += app.LogWarnings
}
writeDebugJSON(w, http.StatusOK, true,
fmt.Sprintf("Telemetria összegyűjtve: %d app, %d hiba, %d figyelmeztetés (%dms)",
len(telemetry), totalErrors, totalWarnings, latency),
map[string]interface{}{
"latency_ms": latency,
"app_count": len(telemetry),
"total_errors": totalErrors,
"total_warnings": totalWarnings,
"app_telemetry": telemetry,
})
}
// ── Section 6: Self-update ──────────────────────────────────────────
func (s *Server) debugSelfUpdateDryRun(w http.ResponseWriter, r *http.Request) {
@@ -127,6 +127,22 @@
</div>
</div>
<!-- Section: Telemetry Testing -->
<div class="card debug-section" id="section-telemetry">
<div class="card-header debug-section-header" onclick="toggleSection('telemetry')">
<h3>Telemetria teszt</h3>
<span class="section-toggle"></span>
</div>
<div class="card-body debug-section-body" style="display:none">
<div id="telemetry-status"><span class="text-muted">Kattintson a gombra a telemetria futtatásához.</span></div>
<div class="debug-actions">
<button class="btn btn-primary btn-sm" id="btn-telemetry-run" data-label="Telemetria futtatása" onclick="runTelemetryTest()">Telemetria futtatása</button>
<span class="debug-result" id="btn-telemetry-run-result"></span>
</div>
<div id="telemetry-detail" style="display:none; margin-top:1rem;"></div>
</div>
</div>
<!-- Section 6: Self-Update Testing -->
<div class="card debug-section" id="section-selfupdate">
<div class="card-header debug-section-header" onclick="toggleSection('selfupdate')">
@@ -266,6 +282,7 @@ function loadSectionData(id) {
case 'backup': loadBackupStatus(); break;
case 'storage': loadWatchdogStatus(); break;
case 'hub': loadHubStatus(); break;
case 'telemetry': break; // no auto-load, user triggers manually
case 'selfupdate': loadSelfUpdateStatus(); break;
case 'dr': loadDRStatus(); break;
case 'logs': initLogViewer(); break;
@@ -597,6 +614,85 @@ function clearLogDisplay() {
lastLogTimestamp = '';
}
// ── Telemetry test ──
function runTelemetryTest() {
var btn = document.getElementById('btn-telemetry-run');
var result = document.getElementById('btn-telemetry-run-result');
var detail = document.getElementById('telemetry-detail');
btn.disabled = true;
btn.textContent = 'Folyamatban...';
result.className = 'debug-result';
result.textContent = '';
detail.style.display = 'none';
fetch('/api/debug/telemetry', {headers: csrfHeaders()}).then(function(r){return r.json()}).then(function(data) {
if (data.ok) {
result.className = 'debug-result debug-result-ok';
result.textContent = data.message;
if (data.data && data.data.app_telemetry) {
renderTelemetryDetail(data.data);
}
} else {
result.className = 'debug-result debug-result-error';
result.textContent = data.error || 'Hiba';
}
}).catch(function(e) {
result.className = 'debug-result debug-result-error';
result.textContent = 'Hálózati hiba: ' + e.message;
}).finally(function() {
btn.disabled = false;
btn.textContent = btn.dataset.label;
});
}
function renderTelemetryDetail(data) {
var detail = document.getElementById('telemetry-detail');
var apps = data.app_telemetry || [];
if (apps.length === 0) {
detail.innerHTML = '<span class="text-muted">Nincs telepített alkalmazás vagy nincs mérési adat.</span>';
detail.style.display = 'block';
return;
}
var html = '<table class="debug-table" style="width:100%;font-size:.85rem">';
html += '<thead><tr><th>Alkalmazás</th><th>Konténerek</th><th>Memória (jelen.)</th><th>Memória (átlag)</th><th>Memória (csúcs)</th><th>CPU (átlag)</th><th>Katalógus limit</th><th>Hibák</th><th>Figyelmeztetések</th></tr></thead><tbody>';
for (var i = 0; i < apps.length; i++) {
var a = apps[i];
var errorClass = a.log_errors > 0 ? ' style="color:var(--red);font-weight:600"' : '';
var warnClass = a.log_warnings > 0 ? ' style="color:var(--yellow);font-weight:600"' : '';
html += '<tr>';
html += '<td><strong>' + escapeHtml(a.display_name || a.app_name) + '</strong></td>';
html += '<td class="mono" style="font-size:.8rem">' + (a.containers||[]).map(escapeHtml).join(', ') + '</td>';
html += '<td>' + (a.memory_current_mb||0).toFixed(1) + ' MB</td>';
html += '<td>' + (a.memory_avg_mb||0).toFixed(1) + ' MB</td>';
html += '<td>' + (a.memory_peak_mb||0).toFixed(1) + ' MB</td>';
html += '<td>' + (a.cpu_avg_percent||0).toFixed(1) + '%</td>';
html += '<td class="mono">' + escapeHtml(a.catalog_limit || '-') + '</td>';
html += '<td' + errorClass + '>' + (a.log_errors||0) + '</td>';
html += '<td' + warnClass + '>' + (a.log_warnings||0) + '</td>';
html += '</tr>';
if (a.issues && a.issues.length > 0) {
for (var j = 0; j < a.issues.length; j++) {
var issue = a.issues[j];
var sevColor = issue.severity === 'error' ? 'var(--red)' : 'var(--yellow)';
html += '<tr style="font-size:.8rem;opacity:.85"><td></td>';
html += '<td colspan="6" style="padding-left:1.5rem"><span style="color:' + sevColor + '">' + escapeHtml(issue.severity.toUpperCase()) + '</span> ' + escapeHtml(issue.message) + '</td>';
html += '<td colspan="2">×' + (issue.count||0) + '</td></tr>';
}
}
}
html += '</tbody></table>';
html += '<details style="margin-top:1rem"><summary class="text-muted" style="cursor:pointer;font-size:.85rem">Nyers JSON</summary>';
html += '<pre class="mono" style="font-size:.75rem;max-height:400px;overflow:auto;padding:.5rem;background:rgba(0,0,0,.3);border-radius:.25rem;margin-top:.5rem">' + escapeHtml(JSON.stringify(apps, null, 2)) + '</pre>';
html += '</details>';
detail.innerHTML = html;
detail.style.display = 'block';
}
// ── Helpers ──
function fmtTime(ts) {
if (!ts) return '-';