Add improved health check and expose it for docker (#373)
This commit is contained in:
parent
c6c2491630
commit
1dff1a9f13
11 changed files with 182 additions and 5 deletions
|
|
@ -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
59
cmd/health.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
49
core/health.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
1
go.mod
|
|
@ -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
2
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
41
server/uds.go
Normal 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
8
server/uds_windows.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// +build windows
|
||||
|
||||
package server
|
||||
|
||||
// HealthListener attaches listener to unix domain socket
|
||||
func HealthListener(site site) {
|
||||
// nop
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue