feat: complete Windows client control and recovery paths
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
// Package observability provides the intentionally small health/metrics HTTP
|
||||
// surface shared by the Linux server and Windows client. It contains no
|
||||
// product state and never exposes command payloads or high-cardinality IDs.
|
||||
package observability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Paths struct {
|
||||
Liveness string
|
||||
Readiness string
|
||||
Metrics string
|
||||
}
|
||||
|
||||
type Health struct {
|
||||
ready atomic.Bool
|
||||
dirty atomic.Bool
|
||||
mu sync.Mutex
|
||||
count map[string]uint64
|
||||
}
|
||||
|
||||
func New() *Health { return &Health{count: make(map[string]uint64)} }
|
||||
|
||||
func (health *Health) SetReady(value bool) {
|
||||
if health != nil {
|
||||
health.ready.Store(value)
|
||||
}
|
||||
}
|
||||
|
||||
func (health *Health) SetDirty(value bool) {
|
||||
if health != nil {
|
||||
health.dirty.Store(value)
|
||||
}
|
||||
}
|
||||
|
||||
func (health *Health) Inc(name string) {
|
||||
if health == nil || !validMetricName(name) {
|
||||
return
|
||||
}
|
||||
health.mu.Lock()
|
||||
health.count[name]++
|
||||
health.mu.Unlock()
|
||||
}
|
||||
|
||||
func (health *Health) Snapshot() (ready, dirty bool, counters map[string]uint64) {
|
||||
if health == nil {
|
||||
return false, true, nil
|
||||
}
|
||||
health.mu.Lock()
|
||||
defer health.mu.Unlock()
|
||||
copyCounters := make(map[string]uint64, len(health.count))
|
||||
for key, value := range health.count {
|
||||
copyCounters[key] = value
|
||||
}
|
||||
return health.ready.Load(), health.dirty.Load(), copyCounters
|
||||
}
|
||||
|
||||
func (health *Health) Handler(paths Paths) http.Handler {
|
||||
if paths.Liveness == "" {
|
||||
paths.Liveness = "/livez"
|
||||
}
|
||||
if paths.Readiness == "" {
|
||||
paths.Readiness = "/readyz"
|
||||
}
|
||||
if paths.Metrics == "" {
|
||||
paths.Metrics = "/metrics"
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc(paths.Liveness, func(response http.ResponseWriter, _ *http.Request) {
|
||||
response.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
response.WriteHeader(http.StatusOK)
|
||||
_, _ = response.Write([]byte("live\n"))
|
||||
})
|
||||
mux.HandleFunc(paths.Readiness, func(response http.ResponseWriter, _ *http.Request) {
|
||||
ready, dirty, _ := health.Snapshot()
|
||||
response.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
if !ready {
|
||||
response.WriteHeader(http.StatusServiceUnavailable)
|
||||
} else {
|
||||
response.WriteHeader(http.StatusOK)
|
||||
}
|
||||
_, _ = fmt.Fprintf(response, "ready=%s dirty=%s\n", strconv.FormatBool(ready), strconv.FormatBool(dirty))
|
||||
})
|
||||
mux.HandleFunc(paths.Metrics, func(response http.ResponseWriter, _ *http.Request) {
|
||||
ready, dirty, counters := health.Snapshot()
|
||||
response.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
||||
response.WriteHeader(http.StatusOK)
|
||||
_, _ = fmt.Fprintf(response, "rvbox_health_ready %d\nrvbox_health_dirty %d\n", boolMetric(ready), boolMetric(dirty))
|
||||
keys := make([]string, 0, len(counters))
|
||||
for key := range counters {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
_, _ = fmt.Fprintf(response, "rvbox_%s_total %d\n", key, counters[key])
|
||||
}
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
// Serve starts an endpoint and closes it when ctx is cancelled. The caller
|
||||
// may use the returned error to keep listener failures visible without making
|
||||
// product startup depend on a slow or unavailable metrics consumer.
|
||||
func (health *Health) Serve(ctx context.Context, listen string, paths Paths) error {
|
||||
if strings.TrimSpace(listen) == "" {
|
||||
return fmt.Errorf("observability listen address is empty")
|
||||
}
|
||||
server := &http.Server{Addr: listen, Handler: health.Handler(paths), ReadHeaderTimeout: 10 * time.Second}
|
||||
listener, err := net.Listen("tcp", listen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = server.Shutdown(context.Background())
|
||||
}()
|
||||
err = server.Serve(listener)
|
||||
if err == http.ErrServerClosed {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func boolMetric(value bool) int {
|
||||
if value {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func validMetricName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
for _, character := range name {
|
||||
if !(character == '_' || character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' || character >= '0' && character <= '9') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user