Browse Source

Health status file type

Ask Bjørn Hansen 8 years ago
parent
commit
944363428a
5 changed files with 214 additions and 31 deletions
  1. 25 22
      health/status.go
  2. 155 0
      health/status_file.go
  3. 19 0
      health/status_test.go
  4. 1 5
      zones/muxmanager.go
  5. 14 4
      zones/reader.go

+ 25 - 22
health/status.go

@@ -1,6 +1,7 @@
 package health
 
 import (
+	"fmt"
 	"strings"
 	"sync"
 )
@@ -11,14 +12,15 @@ import (
 type StatusType uint8
 
 const (
-	StatusUnhealthy StatusType = iota
+	StatusUnknown StatusType = iota
+	StatusUnhealthy
 	StatusHealthy
-	StatusUnknown
 )
 
 type Status interface {
-	// Load(string) error
 	GetStatus(string) StatusType
+	Reload() error
+	Close() error
 }
 
 type statusRegistry struct {
@@ -32,17 +34,32 @@ type Service struct {
 	Status StatusType
 }
 
-type StatusFile struct {
-	mu sync.RWMutex
-	m  map[string]*Service
-}
-
 func init() {
 	registry = statusRegistry{
 		m: make(map[string]Status),
 	}
 }
 
+func (r *statusRegistry) Add(name string, status Status) error {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	r.m[name] = status
+	return nil
+}
+
+func (st StatusType) String() string {
+	switch st {
+	case StatusHealthy:
+		return "healthy"
+	case StatusUnhealthy:
+		return "unhealthy"
+	case StatusUnknown:
+		return "unknown"
+	default:
+		return fmt.Sprintf("status=%d", st)
+	}
+}
+
 func GetStatus(name string) StatusType {
 	check := strings.SplitN(name, "/", 2)
 	if len(check) != 2 {
@@ -57,17 +74,3 @@ func GetStatus(name string) StatusType {
 	}
 	return status.GetStatus(check[1])
 }
-
-func NewStatusFile() *StatusFile {
-	return &StatusFile{
-		m: make(map[string]*Service),
-	}
-}
-
-func (s *StatusFile) Load(filename string) error {
-	return nil
-}
-
-func (s *StatusFile) GetStatus(check string) StatusType {
-	return StatusUnknown
-}

+ 155 - 0
health/status_file.go

@@ -0,0 +1,155 @@
+package health
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"path"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/kr/pretty"
+)
+
+type StatusFile struct {
+	filename string
+	mu       sync.RWMutex
+	m        StatusFileData
+}
+
+type StatusFileData map[string]*Service
+
+func NewStatusFile(filename string) *StatusFile {
+	return &StatusFile{
+		m:        make(StatusFileData),
+		filename: filename,
+	}
+}
+
+func DirectoryReader(dir string) {
+	for {
+		err := reloadDirectory(dir)
+		if err != nil {
+			log.Printf("loading health data: %s", err)
+		}
+		time.Sleep(3 * time.Second)
+	}
+}
+
+func reloadDirectory(dir string) error {
+	dirlist, err := ioutil.ReadDir(dir)
+	if err != nil {
+		return fmt.Errorf("could not read '%s': %s", dir, err)
+	}
+
+	seen := map[string]bool{}
+
+	var parseErr error
+
+	for _, file := range dirlist {
+		fileName := file.Name()
+		if !strings.HasSuffix(strings.ToLower(fileName), ".json") ||
+			strings.HasPrefix(path.Base(fileName), ".") ||
+			file.IsDir() {
+			continue
+		}
+		statusName := fileName[0:strings.LastIndex(fileName, ".")]
+
+		registry.mu.Lock()
+		s, ok := registry.m[statusName]
+		registry.mu.Unlock()
+
+		seen[statusName] = true
+
+		if ok {
+			s.Reload()
+		} else {
+			s := NewStatusFile(path.Join(dir, fileName))
+			err := s.Reload()
+			if err != nil {
+				log.Printf("error loading '%s': %s", fileName, err)
+				parseErr = err
+			}
+			registry.Add(statusName, s)
+		}
+	}
+
+	registry.mu.Lock()
+	for n, _ := range registry.m {
+		if !seen[n] {
+			registry.m[n].Close()
+			delete(registry.m, n)
+		}
+	}
+	registry.mu.Unlock()
+
+	return parseErr
+}
+
+func (s *StatusFile) Reload() error {
+	if len(s.filename) > 0 {
+		return s.Load(s.filename)
+	}
+	return nil
+}
+
+// Load imports the data atomically into the status map. If there's
+// a JSON error the old data is preserved.
+func (s *StatusFile) Load(filename string) error {
+	n := StatusFileData{}
+	b, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return err
+	}
+	err = json.Unmarshal(b, &n)
+	if err != nil {
+		return err
+	}
+	s.mu.Lock()
+	s.m = n
+	s.mu.Unlock()
+
+	return nil
+}
+
+func (s *StatusFile) Close() error {
+	s.mu.Lock()
+	s.m = nil
+	s.mu.Unlock()
+	return nil
+}
+
+func (s *StatusFile) GetStatus(check string) StatusType {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	if s.m == nil {
+		return StatusUnknown
+	}
+
+	pretty.Println(s)
+
+	st, ok := s.m[check]
+	if !ok {
+		log.Printf("Not found '%s'", check)
+		return StatusUnknown
+	}
+	return st.Status
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface.
+func (srv *Service) UnmarshalJSON(b []byte) error {
+	var i int64
+	if err := json.Unmarshal(b, &i); err != nil {
+		return err
+	}
+	*srv = Service{Status: StatusType(i)}
+	return nil
+}
+
+// UnmarshalJSON implements the json.Marshaler interface.
+// func (srv *Service) MarshalJSON() ([]byte, error) {
+// return
+// }

+ 19 - 0
health/status_test.go

@@ -0,0 +1,19 @@
+package health
+
+import "testing"
+
+func TestStatusFile(t *testing.T) {
+	sf := NewStatusFile()
+	err := sf.Load("test.json")
+	if err != nil {
+		t.Fatalf("could not load test.json: %s", err)
+	}
+	x := sf.GetStatus("bad")
+
+	t.Logf("bad=%d", x)
+
+	if x != StatusUnhealthy {
+		t.Errorf("'bad' should have been unhealthy but was %s", x.String())
+	}
+	registry.Add("test", sf)
+}

+ 1 - 5
zones/muxmanager.go

@@ -88,7 +88,7 @@ func (mm *MuxManager) reload() error {
 			continue
 		}
 
-		zoneName := zoneNameFromFile(fileName)
+		zoneName := fileName[0:strings.LastIndex(fileName, ".")]
 
 		seenZones[zoneName] = true
 
@@ -193,10 +193,6 @@ func (mm *MuxManager) setupRootZone() {
 	})
 }
 
-func zoneNameFromFile(fileName string) string {
-	return fileName[0:strings.LastIndex(fileName, ".")]
-}
-
 func sha256File(fn string) string {
 	if data, err := ioutil.ReadFile(fn); err != nil {
 		return ""

+ 14 - 4
zones/reader.go

@@ -221,8 +221,15 @@ func setupZoneData(data map[string]interface{}, zone *Zone) {
 				h.Class = dns.ClassINET
 				h.Rrtype = dnsType
 
-				// We add the TTL as a last pass because we might not have
-				// processed it yet when we process the record data.
+				{
+					// allow for individual health test name overrides
+					if rec, ok := records[rType][i].(map[string]interface{}); ok {
+						if h, ok := rec["health"].(string); ok {
+							record.Test = h
+						}
+
+					}
+				}
 
 				switch len(label.Label) {
 				case 0:
@@ -233,7 +240,7 @@ func setupZoneData(data map[string]interface{}, zone *Zone) {
 
 				switch dnsType {
 				case dns.TypeA, dns.TypeAAAA, dns.TypePTR:
-
+					// todo: check interface type
 					str, weight := getStringWeight(records[rType][i].([]interface{}))
 					ip := str
 					record.Weight = weight
@@ -418,7 +425,7 @@ func setupZoneData(data map[string]interface{}, zone *Zone) {
 		}
 	}
 
-	// loop over exisiting labels, create zone records for missing sub-domains
+	// Loop over exisiting labels, create zone records for missing sub-domains
 	// and set TTLs
 	for k := range zone.Labels {
 		if strings.Contains(k, ".") {
@@ -432,6 +439,9 @@ func setupZoneData(data map[string]interface{}, zone *Zone) {
 		}
 		for _, records := range zone.Labels[k].Records {
 			for _, r := range records {
+				// We add the TTL as a last pass because we might not have
+				// processed it yet when we process the record data.
+
 				var defaultTtl uint32 = 86400
 				if r.RR.Header().Rrtype != dns.TypeNS {
 					// NS records have special treatment. If they are not specified, they default to 86400 rather than