feat: enhance /api/v1/status endpoint with full monitoring data #86

Merged
sneak merged 1 commits from feature/enhance-status-endpoint into main 2026-03-10 12:20:12 +01:00
Showing only changes of commit d621799b8e - Show all commits

View File

@@ -2,22 +2,217 @@ package handlers
import (
"net/http"
"sort"
"time"
"sneak.berlin/go/dnswatcher/internal/state"
)
// statusDomainInfo holds status information for a monitored domain.
type statusDomainInfo struct {
Nameservers []string `json:"nameservers"`
LastChecked time.Time `json:"lastChecked"`
}
// statusHostnameNSInfo holds per-nameserver status for a hostname.
type statusHostnameNSInfo struct {
Records map[string][]string `json:"records"`
Status string `json:"status"`
LastChecked time.Time `json:"lastChecked"`
}
// statusHostnameInfo holds status information for a monitored hostname.
type statusHostnameInfo struct {
Nameservers map[string]*statusHostnameNSInfo `json:"nameservers"`
LastChecked time.Time `json:"lastChecked"`
}
// statusPortInfo holds status information for a monitored port.
type statusPortInfo struct {
Open bool `json:"open"`
Hostnames []string `json:"hostnames"`
LastChecked time.Time `json:"lastChecked"`
}
// statusCertificateInfo holds status information for a TLS certificate.
type statusCertificateInfo struct {
CommonName string `json:"commonName"`
Issuer string `json:"issuer"`
NotAfter time.Time `json:"notAfter"`
SubjectAlternativeNames []string `json:"subjectAlternativeNames"`
Status string `json:"status"`
LastChecked time.Time `json:"lastChecked"`
}
// statusCounts holds summary counts of monitored resources.
type statusCounts struct {
Domains int `json:"domains"`
Hostnames int `json:"hostnames"`
Ports int `json:"ports"`
PortsOpen int `json:"portsOpen"`
Certificates int `json:"certificates"`
CertsOK int `json:"certificatesOk"`
CertsError int `json:"certificatesError"`
}
// statusResponse is the full /api/v1/status response.
type statusResponse struct {
Status string `json:"status"`
LastUpdated time.Time `json:"lastUpdated"`
Counts statusCounts `json:"counts"`
Domains map[string]*statusDomainInfo `json:"domains"`
Hostnames map[string]*statusHostnameInfo `json:"hostnames"`
Ports map[string]*statusPortInfo `json:"ports"`
Certificates map[string]*statusCertificateInfo `json:"certificates"`
}
// HandleStatus returns the monitoring status handler.
func (h *Handlers) HandleStatus() http.HandlerFunc {
type response struct {
Status string `json:"status"`
}
return func(
writer http.ResponseWriter,
request *http.Request,
) {
snap := h.state.GetSnapshot()
resp := buildStatusResponse(snap)
h.respondJSON(
writer, request,
&response{Status: "ok"},
resp,
http.StatusOK,
)
}
}
// buildStatusResponse constructs the full status response from
// the current monitoring snapshot.
func buildStatusResponse(
snap state.Snapshot,
) *statusResponse {
resp := &statusResponse{
Status: "ok",
LastUpdated: snap.LastUpdated,
Domains: make(map[string]*statusDomainInfo),
Hostnames: make(map[string]*statusHostnameInfo),
Ports: make(map[string]*statusPortInfo),
Certificates: make(map[string]*statusCertificateInfo),
}
buildDomains(snap, resp)
buildHostnames(snap, resp)
buildPorts(snap, resp)
buildCertificates(snap, resp)
buildCounts(resp)
return resp
}
func buildDomains(
snap state.Snapshot,
resp *statusResponse,
) {
for name, ds := range snap.Domains {
ns := make([]string, len(ds.Nameservers))
copy(ns, ds.Nameservers)
sort.Strings(ns)
resp.Domains[name] = &statusDomainInfo{
Nameservers: ns,
LastChecked: ds.LastChecked,
}
}
}
func buildHostnames(
snap state.Snapshot,
resp *statusResponse,
) {
for name, hs := range snap.Hostnames {
info := &statusHostnameInfo{
Nameservers: make(map[string]*statusHostnameNSInfo),
LastChecked: hs.LastChecked,
}
for ns, nsState := range hs.RecordsByNameserver {
recs := make(map[string][]string, len(nsState.Records))
for rtype, vals := range nsState.Records {
copied := make([]string, len(vals))
copy(copied, vals)
recs[rtype] = copied
}
info.Nameservers[ns] = &statusHostnameNSInfo{
Records: recs,
Status: nsState.Status,
LastChecked: nsState.LastChecked,
}
}
resp.Hostnames[name] = info
}
}
func buildPorts(
snap state.Snapshot,
resp *statusResponse,
) {
for key, ps := range snap.Ports {
hostnames := make([]string, len(ps.Hostnames))
copy(hostnames, ps.Hostnames)
sort.Strings(hostnames)
resp.Ports[key] = &statusPortInfo{
Open: ps.Open,
Hostnames: hostnames,
LastChecked: ps.LastChecked,
}
}
}
func buildCertificates(
snap state.Snapshot,
resp *statusResponse,
) {
for key, cs := range snap.Certificates {
sans := make([]string, len(cs.SubjectAlternativeNames))
copy(sans, cs.SubjectAlternativeNames)
resp.Certificates[key] = &statusCertificateInfo{
CommonName: cs.CommonName,
Issuer: cs.Issuer,
NotAfter: cs.NotAfter,
SubjectAlternativeNames: sans,
Status: cs.Status,
LastChecked: cs.LastChecked,
}
}
}
func buildCounts(resp *statusResponse) {
var portsOpen, certsOK, certsError int
for _, ps := range resp.Ports {
if ps.Open {
portsOpen++
}
}
for _, cs := range resp.Certificates {
switch cs.Status {
case "ok":
certsOK++
case "error":
certsError++
}
}
resp.Counts = statusCounts{
Domains: len(resp.Domains),
Hostnames: len(resp.Hostnames),
Ports: len(resp.Ports),
PortsOpen: portsOpen,
Certificates: len(resp.Certificates),
CertsOK: certsOK,
CertsError: certsError,
}
}