Add comprehensive logging and debug endpoints for production troubleshooting

- Add request logging middleware to main.go
- Add debug handler with health, config, stats, and test-dns endpoints
- Add detailed logging to DynDNS handler
- Add logging to Technitium DNS client
- Add database Ping() and GetStats() methods
- New endpoints:
  - /health - detailed health status with database and DNS checks
  - /debug/config - sanitized configuration
  - /debug/stats - database statistics
  - /debug/test-dns - live DNS test endpoint

This will help diagnose production issues with DNS updates.
This commit is contained in:
2026-02-02 22:14:24 -05:00
parent ad494fa623
commit 01694f66a8
5 changed files with 266 additions and 0 deletions

169
internal/handlers/debug.go Normal file
View File

@@ -0,0 +1,169 @@
package handlers
import (
"fmt"
"net/http"
"os"
"runtime"
"time"
"git.dws.rip/DWS/dyn/internal/config"
"git.dws.rip/DWS/dyn/internal/database"
"git.dws.rip/DWS/dyn/internal/dns"
"github.com/gin-gonic/gin"
)
// DebugHandler provides diagnostic endpoints for production troubleshooting
type DebugHandler struct {
db *database.DB
dns *dns.Client
config *config.Config
startTime string
}
// NewDebugHandler creates a new debug handler
func NewDebugHandler(db *database.DB, dnsClient *dns.Client, cfg *config.Config) *DebugHandler {
return &DebugHandler{
db: db,
dns: dnsClient,
config: cfg,
startTime: time.Now().Format(time.RFC3339),
}
}
// Health returns detailed health status
func (h *DebugHandler) Health(c *gin.Context) {
health := gin.H{
"status": "healthy",
"version": getVersion(),
"uptime": h.startTime,
"go_version": runtime.Version(),
}
// Check database
if err := h.db.Ping(); err != nil {
health["database"] = gin.H{"status": "unhealthy", "error": err.Error()}
health["status"] = "degraded"
} else {
health["database"] = gin.H{"status": "healthy"}
}
// Check Technitium connectivity
if err := h.testTechnitiumConnection(); err != nil {
health["technitium"] = gin.H{"status": "unhealthy", "error": err.Error()}
health["status"] = "degraded"
} else {
health["technitium"] = gin.H{"status": "healthy"}
}
// Config (without secrets)
health["config"] = gin.H{
"base_domain": h.config.BaseDomain,
"space_subdomain": h.config.SpaceSubdomain,
"zone": h.config.GetZone(),
"technitium_url": h.config.TechnitiumURL,
"rate_limit_ip": h.config.RateLimitPerIP,
"rate_limit_token": h.config.RateLimitPerToken,
}
c.JSON(http.StatusOK, health)
}
// TestDNS attempts to create and delete a test DNS record
func (h *DebugHandler) TestDNS(c *gin.Context) {
testSubdomain := "test-health-check-" + fmt.Sprintf("%d", time.Now().Unix())
testIP := "1.2.3.4"
zone := h.config.GetZone()
results := gin.H{
"zone": zone,
"subdomain": testSubdomain,
"test_ip": testIP,
}
// Try to add a record
err := h.dns.AddARecord(zone, testSubdomain, testIP, 60)
if err != nil {
results["add_record"] = gin.H{"success": false, "error": err.Error()}
c.JSON(http.StatusServiceUnavailable, results)
return
}
results["add_record"] = gin.H{"success": true}
// Try to add wildcard
err = h.dns.AddWildcardARecord(zone, testSubdomain, testIP, 60)
if err != nil {
results["add_wildcard"] = gin.H{"success": false, "error": err.Error()}
c.JSON(http.StatusServiceUnavailable, results)
return
}
results["add_wildcard"] = gin.H{"success": true}
// Cleanup - delete the test records
err = h.dns.DeleteRecord(zone, testSubdomain, "A")
if err != nil {
results["cleanup"] = gin.H{"warning": "Failed to cleanup test record", "error": err.Error()}
} else {
results["cleanup"] = gin.H{"success": true}
}
// Delete wildcard
err = h.dns.DeleteRecord(zone, "*."+testSubdomain, "A")
if err != nil {
results["cleanup_wildcard"] = gin.H{"warning": "Failed to cleanup wildcard", "error": err.Error()}
}
results["overall"] = "success"
c.JSON(http.StatusOK, results)
}
// ConfigDebug shows current configuration (sanitized)
func (h *DebugHandler) ConfigDebug(c *gin.Context) {
cfg := gin.H{
"server_port": h.config.ServerPort,
"database_path": h.config.DatabasePath,
"technitium_url": h.config.TechnitiumURL,
"base_domain": h.config.BaseDomain,
"space_subdomain": h.config.SpaceSubdomain,
"zone": h.config.GetZone(),
"rate_limit_per_ip": h.config.RateLimitPerIP,
"rate_limit_per_token": h.config.RateLimitPerToken,
"trusted_proxies": h.config.TrustedProxies,
// Don't expose credentials!
"has_token": h.config.TechnitiumToken != "",
"has_username": h.config.TechnitiumUsername != "",
"has_password": h.config.TechnitiumPassword != "",
}
c.JSON(http.StatusOK, cfg)
}
// Stats returns database statistics
func (h *DebugHandler) Stats(c *gin.Context) {
stats, err := h.db.GetStats()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
func (h *DebugHandler) testTechnitiumConnection() error {
// Try a simple operation - add and immediately delete a test record
testSubdomain := "conn-test" + fmt.Sprintf("%d", time.Now().Unix())
err := h.dns.AddARecord(h.config.GetZone(), testSubdomain, "127.0.0.1", 1)
if err != nil {
return err
}
// Cleanup
h.dns.DeleteRecord(h.config.GetZone(), testSubdomain, "A")
return nil
}
func getVersion() string {
if v := os.Getenv("VERSION"); v != "" {
return v
}
return "dev"
}