From bfe03906ce05db647833eb0e320379f778db8229 Mon Sep 17 00:00:00 2001 From: Kirill Danshin Date: Tue, 6 Nov 2018 23:00:34 +0300 Subject: [PATCH] 1.5.0 Signed-off-by: Kirill Danshin --- app_listenAndServeAutoTLS.go | 7 +- context_proxy.go | 8 ++- docs/CHANGELOG.md | 12 +++- healthchecks/hc.go | 23 ++++++ healthchecks/hc_internal.go | 136 +++++++++++++++++++++++++++++++++++ internal/gfmt/si.go | 41 +++++++++++ subroute.go | 2 +- version.go | 2 +- 8 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 healthchecks/hc.go create mode 100644 healthchecks/hc_internal.go create mode 100644 internal/gfmt/si.go diff --git a/app_listenAndServeAutoTLS.go b/app_listenAndServeAutoTLS.go index bcd9c40..d236d4c 100644 --- a/app_listenAndServeAutoTLS.go +++ b/app_listenAndServeAutoTLS.go @@ -70,12 +70,7 @@ func (app *App) ListenAndServeAutoTLS(addr string, cachePath ...string) error { } tlsConfig := getDefaultTLSConfig() - tlsConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { - var ( - cert *tls.Certificate - err error - ) - + tlsConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (cert *tls.Certificate, err error) { if len(hello.ServerName) == 0 || hello.ServerName == localhost { hello.ServerName = localhost cert, err = selfSignedCertificate(hello) diff --git a/context_proxy.go b/context_proxy.go index 3dc6db4..c751990 100644 --- a/context_proxy.go +++ b/context_proxy.go @@ -14,9 +14,13 @@ import ( ) // Proxy request to given url -func (ctx *Context) Proxy(url string) error { +func (ctx *Context) Proxy(url string) (err error) { proxyReq := fasthttp.AcquireRequest() ctx.Request.CopyTo(proxyReq) proxyReq.SetRequestURI(url) - return fasthttp.Do(proxyReq, &ctx.Response) + + err = fasthttp.Do(proxyReq, &ctx.Response) + + fasthttp.ReleaseRequest(proxyReq) + return } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1c86a1e..6e7826f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,19 +1,25 @@ +# Patch release: 1.5.0 +- Separate store static route. Allows both `/post/:id` and `/post/about` routes +- Healtchecks module added +- Fix resource usage leak in ctx.Proxy() +- `Sub()` now allows to convert `App`'s root router to a SubRouter type + # Patch release: 1.4.2 - Setting of cookie path fixed # Patch release: 1.4.1 -- Regression fixed: empty app name. Now if no `OptAppName` provided `App.name` will be set to default +- Regression fixed: empty app name. Now if no `OptAppName` provided `App.name` will fallback to default setting # Minor release: 1.4.0 - Added `OptAppName` option for App initializer - Fixed ability to set empty `""` server name - Fixed `fasthttp.Server` name set via `App.SetName()` - Method `App.SetName()` market as deprecated in favor of `OptAppName` -- Methods `App.ListenAndServeAllDev()` & `App.ListenAndServeAutoTLSDev()` marked as deprecated and from now is simple aliases of `App.ListenAndServeAll()` & `App.ListenAndServeAutoTLS()` accordingly +- `App.ListenAndServeAllDev()` and `App.ListenAndServeAutoTLSDev()` methods marked as deprecated and from now is simple aliases of `App.ListenAndServeAll()` and `App.ListenAndServeAutoTLS()` accordingly - Fixed `go.mod` dependencies and `go mod vendor` applied to support older versions of GO # Patch release: 1.3.2 -- Added `SubRouter.Handle()` method similar to `Router.Handle()` & `App.Handle()`. +- Added `SubRouter.Handle()` method with the same behaviour as `Router.Handle()` and `App.Handle()`. # Patch release: 1.3.1 - Fix healthcheck formatting diff --git a/healthchecks/hc.go b/healthchecks/hc.go new file mode 100644 index 0000000..88e8098 --- /dev/null +++ b/healthchecks/hc.go @@ -0,0 +1,23 @@ +package healthchecks + +// Register both ping and healthcheck endpoints +func Register(r interface{}, collectors ...func() (statKey string, stats interface{})) error { + return doReg(r, collectors, true, true) +} + +// RegisterPing registers ping endpoint +func RegisterPing(r interface{}) error { + return doReg(r, nil, true, false) +} + +// RegisterHealthcheck registers healthcheck endpoint +func RegisterHealthcheck(r interface{}, collectors ...func() (statKey string, stats interface{})) error { + return doReg(r, collectors, false, true) +} + +// ServeHealthcheck serves healthcheck +func ServeHealthcheck(collectors ...func() (statKey string, stats interface{})) func() interface{} { + return func() interface{} { + return check(collectors...) + } +} diff --git a/healthchecks/hc_internal.go b/healthchecks/hc_internal.go new file mode 100644 index 0000000..3fc9b55 --- /dev/null +++ b/healthchecks/hc_internal.go @@ -0,0 +1,136 @@ +package healthchecks + +import ( + "errors" + "runtime" + "strings" + + sigar "github.com/cloudfoundry/gosigar" + "github.com/gramework/gramework" + "github.com/gramework/gramework/internal/gfmt" +) + +type routerable interface{} + +type hc struct { + CPUClock string `json:"cpu_clock"` + RAM ramJSON `json:"ram_usage"` + Swap ramJSON `json:"swap_usage"` + + LA laJSON `json:"load_average"` + LoadStatus string `json:"load_alert_status"` + + Uptime string `json:"uptime"` + + Custom map[string]interface{} `json:"custom_metrics,omitempty"` +} + +type laJSON struct { + One float64 `json:"one"` + Five float64 `json:"five"` + Fifteen float64 `json:"fifteen"` +} + +type ramJSON struct { + Used string `json:"used"` + Total string `json:"total"` +} + +type sigarWrapper struct { + sigar.ConcreteSigar +} + +func (s sigarWrapper) swap() ramJSON { + swap, err := s.GetSwap() + if err != nil { + swap.Get() + } + return ramJSON{ + Used: gfmt.Si(swap.Used), + Total: gfmt.Si(swap.Total), + } +} +func (s sigarWrapper) ram() ramJSON { + mem, err := s.GetMem() + if err != nil { + mem.Get() + } + return ramJSON{ + Used: gfmt.Si(mem.Used), + Total: gfmt.Si(mem.Total), + } +} + +func doReg(r interface{}, collectors []func() (statKey string, stats interface{}), registerPing, registerHC bool) error { + app, isApp := r.(*gramework.App) + sr, isSr := r.(*gramework.SubRouter) + if !isApp && !isSr { + return errors.New("unsupported handler type") + } + + if isApp { + sr = app.Sub("") + } + + if registerPing { + sr.GET("/ping", "pong") + } + if registerHC { + sr.GET("/healthcheck", ServeHealthcheck(collectors...)) + } + + return nil +} + +func check(collectors ...func() (statKey string, stats interface{})) interface{} { + s := sigarWrapper{sigar.ConcreteSigar{}} + currentCheck := &hc{ + CPUClock: gfmt.Si(uint64(gramework.TicksPerSecond())), + RAM: s.ram(), + Swap: s.swap(), + LoadStatus: "", + } + la, err := s.GetLoadAverage() + if err != nil { + err = la.Get() // retry + } + + if err == nil { + maxLA := float64(runtime.NumCPU() + 2) + currentCheck.LA = laJSON(la) + diffOne := maxLA - la.One + diffFive := maxLA - la.Five + diffFifteen := maxLA - la.Fifteen + + alertTrigger := float64(-3) + warnTrigger := float64(0) + + if diffOne < alertTrigger || diffFive < alertTrigger || diffFifteen < alertTrigger { + currentCheck.LoadStatus = "alert" + } else if diffOne < warnTrigger || diffFive < warnTrigger || diffFifteen < warnTrigger { + currentCheck.LoadStatus = "warn" + } else { + currentCheck.LoadStatus = "ok" + } + } + + uptime := sigar.Uptime{} + err = uptime.Get() + if err != nil { + err = uptime.Get() // retry + } + if err == nil { + currentCheck.Uptime = strings.TrimSpace(uptime.Format()) + } + + if len(collectors) > 0 { + currentCheck.Custom = make(map[string]interface{}) + } + for _, cb := range collectors { + if cb != nil { + key, stats := cb() + currentCheck.Custom[key] = stats + } + } + return currentCheck +} diff --git a/internal/gfmt/si.go b/internal/gfmt/si.go new file mode 100644 index 0000000..8c2b59e --- /dev/null +++ b/internal/gfmt/si.go @@ -0,0 +1,41 @@ +package gfmt + +import "fmt" + +// Si formats a number in a short si format +func Si(n uint64) string { + suff := siRaw + x := float64(n) + for ; x >= 1024; x = x / 1024 { + suff++ + } + + return fmt.Sprintf("%.2f%s", x, suff.String()) +} + +type siSuff uint + +const ( + siRaw siSuff = iota + siKilo + siMega + siGiga + siTera +) + +func (s siSuff) String() string { + switch s { + case siRaw: + return "" + case siKilo: + return "K" + case siMega: + return "M" + case siGiga: + return "G" + case siTera: + return "T" + default: + return "T" + } +} diff --git a/subroute.go b/subroute.go index a699082..0a25bd2 100644 --- a/subroute.go +++ b/subroute.go @@ -136,7 +136,7 @@ func (r *SubRouter) Sub(path string) *SubRouter { } func (r *SubRouter) prefixedRoute(route string) string { - if r.prefix[len(r.prefix)-1] != '/' && route[0] != '/' { + if len(r.prefix) > 0 && r.prefix[len(r.prefix)-1] != '/' && route[0] != '/' { return fmt.Sprintf("%s/%s", r.prefix, route) } diff --git a/version.go b/version.go index c93add3..115cc1f 100644 --- a/version.go +++ b/version.go @@ -11,6 +11,6 @@ package gramework // Version gives you the gramework version you use now const ( - Version = "1.4.2" + Version = "1.5.0" DefaultAppName = "gramework/" + Version )