diff --git a/service.go b/service.go index f085895..b4fd7ef 100644 --- a/service.go +++ b/service.go @@ -95,6 +95,20 @@ type HealthResult struct { TreeRoot string `json:"tree_root"` } +// ServiceDescription is a semantic snapshot of the service state. +// +// snapshot := service.Describe() +// fmt.Println(snapshot.Status, snapshot.ZoneApex, snapshot.TreeRoot) +type ServiceDescription struct { + Status string `json:"status"` + Records int `json:"records"` + ZoneApex string `json:"zone_apex"` + TreeRoot string `json:"tree_root"` + DNSPort int `json:"dns_port"` + HTTPPort int `json:"http_port"` + RecordTTL string `json:"record_ttl"` +} + type Service struct { mu sync.RWMutex records map[string]NameRecords @@ -1104,6 +1118,48 @@ func (service *Service) Health() HealthResult { } } +// Describe returns a structured snapshot that is easier to inspect than String(). +// +// snapshot := service.Describe() +// fmt.Printf("%+v\n", snapshot) +func (service *Service) Describe() ServiceDescription { + if service == nil { + return ServiceDescription{ + Status: "not_ready", + } + } + + service.pruneExpiredRecords() + + service.mu.RLock() + defer service.mu.RUnlock() + + treeRoot := service.treeRoot + if service.chainTreeRoot != "" { + treeRoot = service.chainTreeRoot + } + + dnsPort := service.dnsPort + if dnsPort <= 0 { + dnsPort = DefaultDNSPort + } + + httpPort := service.httpPort + if httpPort <= 0 { + httpPort = DefaultHTTPPort + } + + return ServiceDescription{ + Status: "ready", + Records: len(service.records), + ZoneApex: service.zoneApex, + TreeRoot: treeRoot, + DNSPort: dnsPort, + HTTPPort: httpPort, + RecordTTL: service.recordTTL.String(), + } +} + // ZoneApex returns the computed apex for the current record set. // // apex := service.ZoneApex() @@ -1402,14 +1458,16 @@ func NormalizeName(name string) string { // // fmt.Println(service) func (service *Service) String() string { - service.mu.RLock() - defer service.mu.RUnlock() - + snapshot := service.Describe() return fmt.Sprintf( - "dns.Service{records=%d zone_apex=%q tree_root=%q}", - len(service.records), - service.zoneApex, - service.treeRoot, + "dns.Service{status=%q records=%d zone_apex=%q tree_root=%q dns_port=%d http_port=%d record_ttl=%q}", + snapshot.Status, + snapshot.Records, + snapshot.ZoneApex, + snapshot.TreeRoot, + snapshot.DNSPort, + snapshot.HTTPPort, + snapshot.RecordTTL, ) } diff --git a/service_test.go b/service_test.go index a5a21c8..7290311 100644 --- a/service_test.go +++ b/service_test.go @@ -617,6 +617,49 @@ func TestServiceLocalMutationClearsChainTreeRoot(t *testing.T) { } } +func TestServiceDescribeReturnsSemanticSnapshot(t *testing.T) { + service := NewService(ServiceOptions{ + Records: map[string]NameRecords{ + "gateway.charon.lthn": { + A: []string{"10.10.10.10"}, + }, + "node.charon.lthn": { + A: []string{"10.10.10.11"}, + }, + }, + DNSPort: 1053, + HTTPPort: 5555, + }) + + snapshot := service.Describe() + if snapshot.Status != "ready" { + t.Fatalf("expected ready status, got %#v", snapshot.Status) + } + if snapshot.Records != 2 { + t.Fatalf("expected two cached records, got %#v", snapshot.Records) + } + if snapshot.ZoneApex != "charon.lthn" { + t.Fatalf("expected zone apex charon.lthn, got %#v", snapshot.ZoneApex) + } + if snapshot.TreeRoot == "" { + t.Fatal("expected tree root in service snapshot") + } + if snapshot.DNSPort != 1053 || snapshot.HTTPPort != 5555 { + t.Fatalf("expected configured ports in snapshot, got %#v", snapshot) + } + if snapshot.RecordTTL != "0s" { + t.Fatalf("expected zero TTL string for default service, got %#v", snapshot.RecordTTL) + } + + description := service.String() + if !strings.Contains(description, "zone_apex=\"charon.lthn\"") { + t.Fatalf("expected string description to include zone apex, got %q", description) + } + if !strings.Contains(description, "dns_port=1053") || !strings.Contains(description, "http_port=5555") { + t.Fatalf("expected string description to include configured ports, got %q", description) + } +} + func TestServiceServeHTTPHealthReturnsJSON(t *testing.T) { service := NewService(ServiceOptions{ Records: map[string]NameRecords{