Add improved health check and expose it for docker (#373)

This commit is contained in:
andig 2020-10-06 21:53:18 +02:00 • committed by GitHub
parent c6c2491630
commit 1dff1a9f13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 182 additions and 5 deletions

View file

@ -43,5 +43,7 @@ EXPOSE 7090/udp
# SMA Energy Manager
EXPOSE 9522/udp
HEALTHCHECK --interval=60s --start-period=60s --timeout=30s --retries=3 CMD [ "evcc", "health" ]
ENTRYPOINT [ "/evcc/entrypoint.sh" ]
CMD [ "evcc" ]

59
cmd/health.go Normal file
View file

@ -0,0 +1,59 @@
// +build !windows
package cmd
import (
"fmt"
"net/http"
"os"
"time"
"github.com/andig/evcc/server"
"github.com/andig/evcc/util"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tv42/httpunix"
)
const serviceName = "evcc"
// healthCmd represents the meter command
var healthCmd = &cobra.Command{
Use: "health",
Short: "Check application health",
Run: runHealth,
}
func init() {
rootCmd.AddCommand(healthCmd)
}
func runHealth(cmd *cobra.Command, args []string) {
util.LogLevel(viper.GetString("log"), viper.GetStringMapString("levels"))
log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit)
u := &httpunix.Transport{
DialTimeout: 100 * time.Millisecond,
RequestTimeout: 1 * time.Second,
ResponseHeaderTimeout: 1 * time.Second,
}
u.RegisterLocation(serviceName, server.SocketPath)
var client = http.Client{
Transport: u,
}
var ok bool
resp, err := client.Get(fmt.Sprintf("http+unix://%s/health", serviceName))
if err == nil && resp.StatusCode == http.StatusOK {
log.INFO.Printf("health check ok")
ok = true
}
if !ok {
log.ERROR.Printf("health check failed")
os.Exit(1)
}
}

View file

@ -184,5 +184,8 @@ func run(cmd *cobra.Command, args []string) {
site.DumpConfig()
go site.Run(conf.Interval)
// uds health check listener
go server.HealthListener(site)
log.FATAL.Println(httpd.ListenAndServe())
}

49
core/health.go Normal file
View file

@ -0,0 +1,49 @@
package core
import (
"sync/atomic"
"time"
)
// Health is a health checker that needs regular updates to stay healthy
type Health struct {
locker uint32 // mutex
updated time.Time
timeout time.Duration
}
// NewHealth creates new health checker
func NewHealth(timeout time.Duration) (health *Health) {
return &Health{timeout: timeout}
}
// Healthy returns health status based on last update timestamp
func (health *Health) Healthy() bool {
start := time.Now()
for time.Since(start) < time.Second {
if atomic.CompareAndSwapUint32(&health.locker, 0, 1) {
defer atomic.StoreUint32(&health.locker, 0)
return time.Since(health.updated) < health.timeout
}
time.Sleep(50 * time.Millisecond)
}
return false
}
// Update updates the health timer on each loadpoint update
func (health *Health) Update() {
start := time.Now()
for time.Since(start) < time.Second {
if atomic.CompareAndSwapUint32(&health.locker, 0, 1) {
health.updated = time.Now()
atomic.StoreUint32(&health.locker, 0)
return
}
time.Sleep(50 * time.Millisecond)
}
}

View file

@ -24,6 +24,8 @@ type Site struct {
uiChan chan<- util.Param // client push messages
lpUpdateChan chan *LoadPoint
*Health
log *util.Logger
// configuration
@ -92,6 +94,7 @@ func NewSiteFromConfig(
func NewSite() *Site {
lp := &Site{
log: util.NewLogger("core"),
Health: NewHealth(60 * time.Second),
Voltage: 230, // V
}
@ -122,7 +125,7 @@ type LoadpointConfiguration struct {
TargetSoC int `json:"targetSoC"`
}
// GetMode Gets loadpoint charge mode
// GetMode gets loadpoint charge mode
func (site *Site) GetMode() api.ChargeMode {
return site.loadpoints[0].GetMode()
}
@ -340,6 +343,7 @@ func (site *Site) update(lp Updater) {
if sitePower, err := site.sitePower(); err == nil {
lp.Update(sitePower)
site.Health.Update()
}
}

View file

@ -35,5 +35,7 @@ COPY docker/bin/* /evcc/
EXPOSE 7070
HEALTHCHECK --interval=60s --start-period=60s --timeout=30s --retries=3 CMD [ "evcc", "health" ]
ENTRYPOINT [ "/evcc/entrypoint.sh" ]
CMD [ "evcc" ]

1
go.mod
View file

@ -41,6 +41,7 @@ require (
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1 // indirect
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
github.com/volkszaehler/mbmd v0.0.0-20200831092453-b235d6a65b21
golang.org/x/net v0.0.0-20200707034311-ab3426394381
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect

2
go.sum
View file

@ -389,6 +389,8 @@ github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIU
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=

View file

@ -32,6 +32,7 @@ type route struct {
// site is the minimal interface for accessing site methods
type site interface {
Healthy() bool
Configuration() core.SiteConfiguration
LoadPoints() []*core.LoadPoint
loadpoint
@ -100,10 +101,15 @@ func jsonResponse(w http.ResponseWriter, r *http.Request, content interface{}) {
}
// HealthHandler returns current charge mode
func HealthHandler() http.HandlerFunc {
func HealthHandler(site site) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res := struct{ OK bool }{OK: true}
jsonResponse(w, r, res)
if !site.Healthy() {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "OK")
}
}
@ -231,7 +237,7 @@ type HTTPd struct {
// NewHTTPd creates HTTP server with configured routes for loadpoint
func NewHTTPd(url string, site site, hub *SocketHub, cache *util.Cache) *HTTPd {
var routes = map[string]route{
"health": {[]string{"GET"}, "/health", HealthHandler()},
"health": {[]string{"GET"}, "/health", HealthHandler(site)},
"config": {[]string{"GET"}, "/config", ConfigHandler(site)},
"templates": {[]string{"GET"}, "/config/templates/{class:[a-z]+}", TemplatesHandler()},
"state": {[]string{"GET"}, "/state", StateHandler(cache)},

41
server/uds.go Normal file
View file

@ -0,0 +1,41 @@
// +build !windows
package server
import (
"net"
"net/http"
"os"
)
// SocketPath is the unix domain socket path
const SocketPath = "/tmp/evcc"
// remoteIfExists deletes file if it exists or fails
func remoteIfExists(file string) {
_, err := os.Stat(file)
if err == nil {
err = os.Remove(file)
}
if err != nil && !os.IsNotExist(err) {
log.FATAL.Fatal(err)
}
}
// HealthListener attaches listener to unix domain socket and runs listener
func HealthListener(site site) {
remoteIfExists(SocketPath)
l, err := net.Listen("unix", SocketPath)
if err != nil {
log.FATAL.Fatal(err)
}
defer l.Close()
mux := http.NewServeMux()
httpd := http.Server{Handler: mux}
mux.HandleFunc("/health", HealthHandler(site))
_ = httpd.Serve(l)
}

8
server/uds_windows.go Normal file
View file

@ -0,0 +1,8 @@
// +build windows
package server
// HealthListener attaches listener to unix domain socket
func HealthListener(site site) {
// nop
}