From 4a6ab4d61cac46a651bb9be7ea2129fb70571af5 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 23 Feb 2026 11:09:06 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 9 ++ controller/README.md | 9 +- controller/cmd/controller/main.go | 3 + controller/internal/report/telemetry.go | 7 ++ controller/internal/web/handler_debug.go | 40 ++++++++ controller/internal/web/templates/debug.html | 96 ++++++++++++++++++++ 6 files changed, 161 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 984af78..d9fbc10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/controller/README.md b/controller/README.md index 528ffc6..8bd09fd 100644 --- a/controller/README.md +++ b/controller/README.md @@ -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 ` (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": "..."}` diff --git a/controller/cmd/controller/main.go b/controller/cmd/controller/main.go index b7d7dab..9d8999c 100644 --- a/controller/cmd/controller/main.go +++ b/controller/cmd/controller/main.go @@ -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) } diff --git a/controller/internal/report/telemetry.go b/controller/internal/report/telemetry.go index 57d346d..30f4b5c 100644 --- a/controller/internal/report/telemetry.go +++ b/controller/internal/report/telemetry.go @@ -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) +} diff --git a/controller/internal/web/handler_debug.go b/controller/internal/web/handler_debug.go index 8323603..867aea1 100644 --- a/controller/internal/web/handler_debug.go +++ b/controller/internal/web/handler_debug.go @@ -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) { diff --git a/controller/internal/web/templates/debug.html b/controller/internal/web/templates/debug.html index bb0803d..8a75d69 100644 --- a/controller/internal/web/templates/debug.html +++ b/controller/internal/web/templates/debug.html @@ -127,6 +127,22 @@ + +
+
+

Telemetria teszt

+ +
+ +
+
@@ -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 = 'Nincs telepített alkalmazás vagy nincs mérési adat.'; + detail.style.display = 'block'; + return; + } + + var html = ''; + html += ''; + + 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 += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += '' + (a.log_errors||0) + ''; + html += '' + (a.log_warnings||0) + ''; + html += ''; + + 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 += ''; + html += ''; + html += ''; + } + } + } + html += '
AlkalmazásKonténerekMemória (jelen.)Memória (átlag)Memória (csúcs)CPU (átlag)Katalógus limitHibákFigyelmeztetések
' + escapeHtml(a.display_name || a.app_name) + '' + (a.containers||[]).map(escapeHtml).join(', ') + '' + (a.memory_current_mb||0).toFixed(1) + ' MB' + (a.memory_avg_mb||0).toFixed(1) + ' MB' + (a.memory_peak_mb||0).toFixed(1) + ' MB' + (a.cpu_avg_percent||0).toFixed(1) + '%' + escapeHtml(a.catalog_limit || '-') + '
' + escapeHtml(issue.severity.toUpperCase()) + ' ' + escapeHtml(issue.message) + '×' + (issue.count||0) + '
'; + + html += '
Nyers JSON'; + html += '
' + escapeHtml(JSON.stringify(apps, null, 2)) + '
'; + html += '
'; + + detail.innerHTML = html; + detail.style.display = 'block'; +} + // ── Helpers ── function fmtTime(ts) { if (!ts) return '-';