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 ## 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) ### v0.28.0 — App Telemetry & Analytics (2026-02-23)
#### Added #### 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. 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 ### 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 #### 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. | | 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. | | 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. | | 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. | | 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). | | 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. | | 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). - **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. - **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. - **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 | | 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/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": "..."}` Response format: `{"ok": true/false, "data": ..., "error": "...", "message": "..."}`
+3
View File
@@ -650,6 +650,9 @@ func main() {
return resp.StatusCode, latency, nil return resp.StatusCode, latency, nil
} }
} }
dc.GetTelemetryPreview = func() ([]report.AppTelemetry, error) {
return report.BuildAppTelemetryForDebug(stackMgr, metricsStore, logger), nil
}
webServer.SetDebugCallbacks(dc) webServer.SetDebugCallbacks(dc)
} }
+7
View File
@@ -117,3 +117,10 @@ func buildAppTelemetry(allStacks []stacks.Stack, telemetry []metrics.ContainerTe
} }
return result 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/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor" "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/stacks"
"gitea.dooplex.hu/admin/felhom-controller/internal/system" "gitea.dooplex.hu/admin/felhom-controller/internal/system"
) )
@@ -27,6 +28,7 @@ type DebugCallbacks struct {
TriggerSetupMode func() error TriggerSetupMode func() error
HubConnectivityTest func() (statusCode int, latencyMs int64, err error) HubConnectivityTest func() (statusCode int, latencyMs int64, err error)
GiteaConnectivityTest 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. // 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: case subpath == "gitea/test-connectivity" && r.Method == http.MethodPost:
s.debugGiteaConnectivity(w, r) s.debugGiteaConnectivity(w, r)
// Section: Telemetry testing
case subpath == "telemetry" && r.Method == http.MethodGet:
s.debugTelemetry(w, r)
// Section 6: Self-update // Section 6: Self-update
case subpath == "selfupdate/dry-run" && r.Method == http.MethodPost: case subpath == "selfupdate/dry-run" && r.Method == http.MethodPost:
s.debugSelfUpdateDryRun(w, r) 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) 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 ────────────────────────────────────────── // ── Section 6: Self-update ──────────────────────────────────────────
func (s *Server) debugSelfUpdateDryRun(w http.ResponseWriter, r *http.Request) { func (s *Server) debugSelfUpdateDryRun(w http.ResponseWriter, r *http.Request) {
@@ -127,6 +127,22 @@
</div> </div>
</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 --> <!-- Section 6: Self-Update Testing -->
<div class="card debug-section" id="section-selfupdate"> <div class="card debug-section" id="section-selfupdate">
<div class="card-header debug-section-header" onclick="toggleSection('selfupdate')"> <div class="card-header debug-section-header" onclick="toggleSection('selfupdate')">
@@ -266,6 +282,7 @@ function loadSectionData(id) {
case 'backup': loadBackupStatus(); break; case 'backup': loadBackupStatus(); break;
case 'storage': loadWatchdogStatus(); break; case 'storage': loadWatchdogStatus(); break;
case 'hub': loadHubStatus(); break; case 'hub': loadHubStatus(); break;
case 'telemetry': break; // no auto-load, user triggers manually
case 'selfupdate': loadSelfUpdateStatus(); break; case 'selfupdate': loadSelfUpdateStatus(); break;
case 'dr': loadDRStatus(); break; case 'dr': loadDRStatus(); break;
case 'logs': initLogViewer(); break; case 'logs': initLogViewer(); break;
@@ -597,6 +614,85 @@ function clearLogDisplay() {
lastLogTimestamp = ''; 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 ── // ── Helpers ──
function fmtTime(ts) { function fmtTime(ts) {
if (!ts) return '-'; if (!ts) return '-';