diff --git a/go.mod b/go.mod index c57a2587..58393053 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 // indirect github.com/json-iterator/go v1.1.10 // indirect github.com/leodido/go-urn v1.2.1 // indirect + github.com/magefile/mage v1.11.0 // indirect github.com/matcornic/hermes/v2 v2.1.0 github.com/mattn/go-runewidth v0.0.10 // indirect github.com/mitchellh/copystructure v1.1.1 // indirect @@ -48,18 +49,18 @@ require ( github.com/rogpeppe/go-internal v1.7.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.1+incompatible - github.com/sirupsen/logrus v1.7.0 + github.com/sirupsen/logrus v1.8.0 github.com/spf13/jwalterweatherman v1.1.0 github.com/ugorji/go v1.2.4 // indirect github.com/vanng822/go-premailer v1.9.0 // indirect github.com/vbauerster/mpb/v5 v5.4.0 github.com/xanzy/go-gitlab v0.44.0 github.com/xhit/go-simple-mail v2.2.2+incompatible - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad + golang.org/x/crypto v0.0.0-20210218145215-b8e89b74b9df golang.org/x/net v0.0.0-20210119194325-5f4716e94777 - golang.org/x/oauth2 v0.0.0-20210210192628-66670185b0cd + golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a - golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect + golang.org/x/sys v0.0.0-20210218155724-8ebf48af031b // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect golang.org/x/text v0.3.5 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect diff --git a/status/README.md b/status/README.md index 236f291a..efbf48fe 100644 --- a/status/README.md +++ b/status/README.md @@ -27,7 +27,8 @@ import ( const ( msgOk = "API is working" - msgKO = "something is not well working" + msgKO = "API is not working" + msgWarn = "something is not well working" ) type EmptyStruct struct{} @@ -39,22 +40,45 @@ func init() { vers = version.NewVersion(version.License_MIT, "Package", "Description", "2017-10-21T00:00:00+0200", "0123456789abcdef", "v0.0-dev", "Author Name", "pfx", EmptyStruct{}, 1) // to create new status, you will need some small function to give data, this is type func : - // FctMessagesAll func() (ok string, ko string, cpt string) - // FctMessageItem func() (ok string, ko string) // FctHealth func() error // FctInfo func() (name, release, build string) - // FctVersion func() version.Version // create new status not as later - sts := status.NewVersionStatus(getVersion, getMessageAll, GetHealth, GetHeader, false) - // add a new component - sts.AddComponent(infoAws, getMessageItem, GetAWSHealth, true, false) - // add a new component - sts.AddComponent(infoLDAP, getMessageItem, GetLDAPHealth, true, false) - + sts := status.NewVersion(vers, msgOk, msgKO, msgWarn) + + // add some middleware before router + sts.MiddlewareAdd(func(context *gin.Context) { + // add here your middleware need to be run before the status route + }) + // register to the router list sts.Register("/status", routers.RouterList.Register) - + + // register to the router list with a group + sts.Register("/v1", "/status", routers.RouterList.Register) + + // add a new component mandatory + sts.ComponentNew( + "myComponentMandatory", + NewComponent(true, infoMandatory, healthMandatory, + func() (msgOk string, msgKo string) { + return msgOk, msgKO + }, + 24 * time.Hour, 5 * time.second, + ), + ) + + // add a new component mandatory + sts.ComponentNew( + "myComponentNotMandatory", + NewComponent(true, infoNotMandatory, healthNotMandatory, + func() (msgOk string, msgKo string) { + return msgOk, msgKO + }, + 24 * time.Hour, 5 * time.second, + ), + ) + // use this func to customize return code for each status sts.SetErrorCode(http.StatusOK, http.StatusInternalServerError, http.StatusAccepted) } @@ -78,26 +102,22 @@ func GetLDAPHealth() error { return nil } -func infoAws() (name, release, build string) { - return "AWS S3 Helper", "v0.1.2.3.4", "" +func infoMandatory() (name, release, build string) { + return "Name of my component mandatory", "v0.1.2.3.4", "abcd1234abcd1234" } -func infoLDAP() (name, release, build string) { - return "OpenLDAP Lib", "v0.1.2.3.4", "" +func healthMandatory() error { + return nil } -func getVersion() version.Version { - return vers +func infoNotMandatory() (name, release, build string) { + return "Name of my component not mandatory", "v0.1.2.3.4", "abcd1234abcd1234" } -func getMessageItem() (ok string, ko string) { - return "all is ok", "there is a mistake somewhere" +func healthNotMandatory() error { + return nil } -func getMessageAll() (ok string, ko string, cptErr string) { - ok, ko = getMessageItem() - return ok, ko, "at least one component is in failed" -} ``` In some case, using init function could make mistake (specially if you need to read flag or config file). diff --git a/status/component.go b/status/component.go new file mode 100644 index 00000000..24b974b4 --- /dev/null +++ b/status/component.go @@ -0,0 +1,66 @@ +/* + * MIT License + * + * Copyright (c) 2021 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package status + +import ( + "time" + + "github.com/gin-gonic/gin" +) + +type CptResponse struct { + InfoResponse + StatusResponse +} + +type Component interface { + Get(x *gin.Context) CptResponse + Clean() +} + +func NewComponent(mandatory bool, info FctInfo, health FctHealth, msg FctMessage, infoCacheDuration, statusCacheDuration time.Duration) Component { + return &cpt{ + i: NewInfo(info, mandatory, infoCacheDuration), + s: NewStatus(health, msg, statusCacheDuration), + } +} + +type cpt struct { + i Info + s Status +} + +func (c *cpt) Get(x *gin.Context) CptResponse { + return CptResponse{ + InfoResponse: c.i.Get(x), + StatusResponse: c.s.Get(x), + } +} + +func (c *cpt) Clean() { + c.i.Clean() + c.s.Clean() +} diff --git a/status/health.go b/status/health.go new file mode 100644 index 00000000..58610768 --- /dev/null +++ b/status/health.go @@ -0,0 +1,124 @@ +/* + * MIT License + * + * Copyright (c) 2021 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package status + +import ( + "time" + + "github.com/gin-gonic/gin" + liblog "github.com/nabbar/golib/logger" +) + +type FctHealth func() error +type FctMessage func() (msgOk string, msgKO string) + +type StatusResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + +func (s *StatusResponse) Clone() StatusResponse { + return StatusResponse{ + Status: s.Status, + Message: s.Message, + } +} + +type Status interface { + Get(x *gin.Context) StatusResponse + Clean() + IsValid() bool +} + +func NewStatus(health FctHealth, msg FctMessage, cacheDuration time.Duration) Status { + return &status{ + fh: health, + fm: msg, + c: nil, + t: time.Time{}, + d: cacheDuration, + } +} + +type status struct { + fh FctHealth + fm FctMessage + + c *StatusResponse + t time.Time + d time.Duration +} + +func (s *status) Get(x *gin.Context) StatusResponse { + if !s.IsValid() { + var ( + err error + msgOk string + msgKO string + ) + + if s.fm != nil { + msgOk, msgKO = s.fm() + } + + if s.fh != nil { + err = s.fh() + } + + c := &StatusResponse{} + + if err != nil { + c.Status = statusKO + c.Message = msgKO + liblog.ErrorLevel.LogGinErrorCtx(liblog.DebugLevel, "get health status", err, x) + } else { + c.Status = statusOK + c.Message = msgOk + } + + s.c = c + s.t = time.Now() + } + + return s.c.Clone() +} + +func (s *status) Clean() { + s.c = nil + s.t = time.Now() +} + +func (s *status) IsValid() bool { + if s.c == nil { + return false + } else if s.t.IsZero() { + return false + } else if time.Since(s.t) > s.d { + return false + } + + return true +} diff --git a/status/info.go b/status/info.go new file mode 100644 index 00000000..bf77bd53 --- /dev/null +++ b/status/info.go @@ -0,0 +1,114 @@ +/* + * MIT License + * + * Copyright (c) 2021 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package status + +import ( + "time" + + "github.com/gin-gonic/gin" +) + +type FctInfo func() (name string, release string, build string) + +type InfoResponse struct { + Name string `json:"name"` + Release string `json:"release"` + HashBuild string `json:"hash_build"` + Mandatory bool `json:"mandatory"` +} + +func (i *InfoResponse) Clone() InfoResponse { + return InfoResponse{ + Name: i.Name, + Release: i.Release, + HashBuild: i.HashBuild, + Mandatory: i.Mandatory, + } +} + +type Info interface { + Get(x *gin.Context) InfoResponse + Clean() + IsValid() bool +} + +func NewInfo(fct FctInfo, mandatory bool, cacheDuration time.Duration) Info { + return &info{ + f: fct, + m: mandatory, + c: nil, + t: time.Time{}, + d: cacheDuration, + } +} + +type info struct { + f FctInfo + m bool + c *InfoResponse + t time.Time + d time.Duration +} + +func (i *info) Get(x *gin.Context) InfoResponse { + if !i.IsValid() { + var ( + name string + vers string + hash string + ) + + if i.f != nil { + name, vers, hash = i.f() + } + + i.c = &InfoResponse{ + Name: name, + Release: vers, + HashBuild: hash, + Mandatory: i.m, + } + i.t = time.Now() + } + + return i.c.Clone() +} + +func (i *info) Clean() { + i.c = nil + i.t = time.Now() +} + +func (i *info) IsValid() bool { + if i.c == nil { + return false + } else if i.t.IsZero() { + return false + } else if time.Since(i.t) > i.d { + return false + } + return true +} diff --git a/status/interface.go b/status/interface.go new file mode 100644 index 00000000..03a71123 --- /dev/null +++ b/status/interface.go @@ -0,0 +1,111 @@ +/* + * MIT License + * + * Copyright (c) 2021 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package status + +import ( + "net/http" + "sync/atomic" + + "github.com/nabbar/golib/version" + + "github.com/gin-gonic/gin" + "github.com/nabbar/golib/router" +) + +const statusOK = "OK" +const statusKO = "KO" + +type Response struct { + InfoResponse + StatusResponse + + Components []CptResponse `json:"components"` +} + +func (r Response) IsOk() bool { + if len(r.Components) < 1 { + return true + } + + for _, c := range r.Components { + if c.Status != statusOK { + return false + } + } + + return true +} + +func (r Response) IsOkMandatory() bool { + if len(r.Components) < 1 { + return true + } + + for _, c := range r.Components { + if !c.Mandatory { + continue + } + + if c.Status != statusOK { + return false + } + } + + return true +} + +type RouteStatus interface { + MiddlewareAdd(mdw ...gin.HandlerFunc) + HttpStatusCode(codeOk, codeKO, codeWarning int) + + Get(c *gin.Context) + Register(prefix string, register router.RegisterRouter) + RegisterGroup(group, prefix string, register router.RegisterRouterInGroup) + + ComponentNew(key string, cpt Component) + ComponentDel(key string) + ComponentDelAll(containKey string) +} + +func New(Name string, Release string, Hash string, msgOk string, msgKo string, msgWarm string) RouteStatus { + return &rtrStatus{ + m: make([]gin.HandlerFunc, 0), + n: Name, + v: Release, + h: Hash, + mOK: msgOk, + cOk: http.StatusOK, + mKO: msgKo, + cKO: http.StatusServiceUnavailable, + mWM: msgWarm, + cWM: http.StatusOK, + c: make(map[string]*atomic.Value, 0), + } +} + +func NewVersion(version version.Version, msgOk string, msgKO string, msgWarm string) RouteStatus { + return New(version.GetPackage(), version.GetRelease(), version.GetBuild(), msgOk, msgKO, msgWarm) +} diff --git a/status/model.go b/status/model.go new file mode 100644 index 00000000..1c163201 --- /dev/null +++ b/status/model.go @@ -0,0 +1,214 @@ +/* + * MIT License + * + * Copyright (c) 2021 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package status + +import ( + "net/http" + "path" + "strings" + "sync/atomic" + + liberr "github.com/nabbar/golib/errors" + liblog "github.com/nabbar/golib/logger" + + "github.com/gin-gonic/gin" + "github.com/nabbar/golib/router" + "github.com/nabbar/golib/semaphore" +) + +type rtrStatus struct { + m []gin.HandlerFunc + + n string + v string + h string + + mOK string + cOk int + mKO string + cKO int + mWM string + cWM int + + c map[string]*atomic.Value +} + +func (r *rtrStatus) HttpStatusCode(codeOk, codeKO, codeWarning int) { + r.cOk = codeOk + r.cKO = codeKO + r.cWM = codeWarning +} + +func (r *rtrStatus) MiddlewareAdd(mdw ...gin.HandlerFunc) { + if len(r.m) < 1 { + r.m = make([]gin.HandlerFunc, 0) + } + + r.m = append(r.m, mdw...) +} + +func (r *rtrStatus) cleanPrefix(prefix string) string { + return path.Clean(strings.TrimRight(path.Join("/", prefix), "/")) +} + +func (r *rtrStatus) Register(prefix string, register router.RegisterRouter) { + prefix = r.cleanPrefix(prefix) + + var m = r.m + m = append(m, r.Get) + register(http.MethodGet, prefix, m...) + + if prefix != "/" { + register(http.MethodGet, prefix+"/", m...) + } +} + +func (r *rtrStatus) RegisterGroup(group, prefix string, register router.RegisterRouterInGroup) { + prefix = r.cleanPrefix(prefix) + + var m = r.m + m = append(m, r.Get) + register(group, http.MethodGet, prefix, m...) + + if prefix != "/" { + register(group, http.MethodGet, prefix+"/", m...) + } +} + +func (r *rtrStatus) Get(x *gin.Context) { + var ( + ok bool + atm *atomic.Value + cpt Component + cid int + key string + err liberr.Error + rsp *Response + sem semaphore.Sem + ) + + defer func() { + if sem != nil { + sem.DeferMain() + } + }() + + rsp = &Response{ + InfoResponse: InfoResponse{ + Name: r.n, + Release: r.v, + HashBuild: r.h, + Mandatory: true, + }, + StatusResponse: StatusResponse{ + Status: statusOK, + Message: r.mOK, + }, + Components: make([]CptResponse, 0), + } + + sem = semaphore.NewSemaphoreWithContext(x, 0) + + for key, atm = range r.c { + if atm == nil { + continue + } + + if cpt, ok = atm.Load().(Component); !ok { + continue + } + + err = sem.NewWorker() + if liblog.ErrorLevel.LogGinErrorCtxf(liblog.DebugLevel, "init new thread to collect data for component '%s'", err, x, key) { + continue + } + + rsp.Components = append(rsp.Components, CptResponse{ + InfoResponse: InfoResponse{}, + StatusResponse: StatusResponse{}, + }) + + cid = len(rsp.Components) - 1 + + go func(id int, c Component) { + defer sem.DeferWorker() + rsp.Components[id] = c.Get(x) + }(cid, cpt) + } + + err = sem.WaitAll() + + if liblog.ErrorLevel.LogGinErrorCtx(liblog.DebugLevel, "waiting all thread to collect data component ", err, x) { + rsp.Message = r.mKO + rsp.Status = statusKO + x.AbortWithStatusJSON(r.cKO, rsp) + } else if !rsp.IsOkMandatory() { + rsp.Message = r.mKO + rsp.Status = statusKO + x.AbortWithStatusJSON(r.cKO, rsp) + } else if !rsp.IsOk() { + rsp.Message = r.mWM + rsp.Status = statusOK + x.JSON(r.cWM, rsp) + } else { + rsp.Message = r.mOK + rsp.Status = statusOK + x.JSON(r.cOk, rsp) + } +} + +func (r *rtrStatus) ComponentNew(key string, cpt Component) { + if len(r.c) < 1 { + r.c = make(map[string]*atomic.Value, 0) + } + + if _, ok := r.c[key]; !ok { + r.c[key] = &atomic.Value{} + } + + r.c[key].Store(cpt) +} + +func (r *rtrStatus) ComponentDel(key string) { + for k := range r.c { + if k == key { + r.c[k].Store(nil) + } + } +} + +func (r *rtrStatus) ComponentDelAll(containKey string) { + if containKey == "" { + r.c = make(map[string]*atomic.Value) + return + } + + for k := range r.c { + if strings.Contains(k, containKey) { + r.c[k].Store(nil) + } + } +} diff --git a/status/status.go b/status/status.go deleted file mode 100644 index 94fdd0bd..00000000 --- a/status/status.go +++ /dev/null @@ -1,436 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2019 Nicolas JUHEL - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package status - -import ( - "fmt" - "net/http" - "path" - "strings" - - "github.com/gin-gonic/gin" - "github.com/nabbar/golib/router" - "github.com/nabbar/golib/semaphore" - "github.com/nabbar/golib/version" -) - -// @TODO : see compliant with https://tools.ietf.org/html/draft-inadarei-api-health-check-02 - -// Model for function that return 3 string for message : -// ok : no error found for component and/or main. -// ko : error found for component and/or main. -// cpt : message for main status message only that say some of component are in error and are mandatory. -type FctMessagesAll func() (ok string, ko string, cpt string) -type FctMessageItem func() (ok string, ko string) - -type FctHealth func() error -type FctInfo func() (name, release, build string) -type FctVersion func() version.Version - -type StatusItemResponse struct { - Name string - Status string - Message string - Release string - HashBuild string -} - -type StatusResponse struct { - StatusItemResponse - Component []StatusItemResponse -} - -const statusOK = "OK" -const statusKO = "KO" - -type statusItem struct { - name string - build string - msgOK string - msgKO string - health func() error - release string -} - -type statusComponent struct { - statusItem - reference string - WarnIfErr bool - later *initLater -} - -type mainPackage struct { - statusItem - msgCptErr string - codeOK int - codeKO int - codeCpt int - cpt []statusComponent - header gin.HandlerFunc - later *initLater - init bool -} - -type initLater struct { - version FctVersion - info FctInfo - msgAll FctMessagesAll - msgItm FctMessageItem - health FctHealth -} - -type Status interface { - Register(prefix string, register router.RegisterRouter) - RegisterGroup(group, prefix string, register router.RegisterRouterInGroup) - - AddComponent(componentRef string, info FctInfo, msg FctMessageItem, health FctHealth, WarnIfError bool, later bool) - AddVersionComponent(componentRef string, vers FctVersion, msg FctMessageItem, health FctHealth, mandatoryComponent bool, later bool) - DelComponent(componentRef string) - - Get(c *gin.Context) - SetErrorCode(codeOk, codeKO, codeCptNoMandatoryKO int) -} - -func NewStatus(info FctInfo, msg FctMessagesAll, health FctHealth, Header gin.HandlerFunc, later bool) Status { - if later { - return &mainPackage{ - cpt: make([]statusComponent, 0), - header: Header, - later: &initLater{ - version: nil, - info: info, - msgItm: nil, - msgAll: msg, - health: health, - }, - codeOK: http.StatusOK, - codeKO: http.StatusServiceUnavailable, - codeCpt: http.StatusAccepted, - init: false, - } - } else { - msgOk, msgKo, msgCpt := msg() - name, rel, build := info() - return &mainPackage{ - statusItem: newItem(name, msgOk, msgKo, rel, build, health), - msgCptErr: msgCpt, - cpt: make([]statusComponent, 0), - header: Header, - later: nil, - init: false, - codeOK: http.StatusOK, - codeKO: http.StatusServiceUnavailable, - codeCpt: http.StatusAccepted, - } - } -} - -func NewVersionStatus(vers FctVersion, msg FctMessagesAll, health FctHealth, Header gin.HandlerFunc, later bool) Status { - if later { - return &mainPackage{ - cpt: make([]statusComponent, 0), - header: Header, - later: &initLater{ - version: vers, - info: nil, - msgItm: nil, - msgAll: msg, - health: health, - }, - init: false, - codeOK: http.StatusOK, - codeKO: http.StatusServiceUnavailable, - codeCpt: http.StatusAccepted, - } - } else { - msgOk, msgKo, msgCpt := msg() - return &mainPackage{ - statusItem: newItem(vers().GetPackage(), msgOk, msgKo, vers().GetRelease(), vers().GetBuild(), health), - msgCptErr: msgCpt, - cpt: make([]statusComponent, 0), - header: Header, - later: nil, - init: false, - codeOK: http.StatusOK, - codeKO: http.StatusServiceUnavailable, - codeCpt: http.StatusAccepted, - } - } -} - -func newItem(name, msgOK, msgKO, release, build string, health FctHealth) statusItem { - return statusItem{ - name: name, - build: build, - msgOK: msgOK, - msgKO: msgKO, - health: health, - release: release, - } -} - -func (p *mainPackage) AddComponent(componentRef string, info FctInfo, msg FctMessageItem, health FctHealth, mandatoryComponent bool, later bool) { - if later { - p.cpt = append(p.cpt, statusComponent{ - reference: componentRef, - WarnIfErr: mandatoryComponent, - later: &initLater{ - version: nil, - info: info, - msgItm: msg, - health: health, - }, - }) - } else { - name, release, build := info() - msgOK, msgKO := msg() - p.cpt = append(p.cpt, statusComponent{ - statusItem: newItem(name, msgOK, msgKO, release, build, health), - reference: componentRef, - WarnIfErr: mandatoryComponent, - later: nil, - }) - } -} - -func (p *mainPackage) AddVersionComponent(componentRef string, vers FctVersion, msg FctMessageItem, health FctHealth, mandatoryComponent bool, later bool) { - if later { - p.cpt = append(p.cpt, statusComponent{ - reference: componentRef, - WarnIfErr: mandatoryComponent, - later: &initLater{ - version: vers, - info: nil, - msgItm: msg, - health: health, - }, - }) - } else { - msgOK, msgKO := msg() - p.cpt = append(p.cpt, statusComponent{ - statusItem: newItem(vers().GetPackage(), msgOK, msgKO, vers().GetRelease(), vers().GetBuild(), health), - reference: componentRef, - WarnIfErr: mandatoryComponent, - later: nil, - }) - } -} - -func (p *mainPackage) DelComponent(componentRef string) { - var new = make([]statusComponent, 0) - - for _, c := range p.cpt { - if c.reference != componentRef { - new = append(new, c) - } - } - - p.cpt = new -} - -func (p *mainPackage) initStatus() { - if p.later != nil { - var ok, ko string - - if p.later.msgAll != nil { - ok, ko, p.msgCptErr = p.later.msgAll() - } else if p.later.msgItm != nil { - ok, ko = p.later.msgItm() - } - - if p.later.info != nil { - name, release, build := p.later.info() - p.statusItem = newItem(name, ok, ko, release, build, p.health) - } else if p.later.version != nil { - vers := p.later.version() - p.statusItem = newItem(vers.GetPackage(), ok, ko, vers.GetRelease(), vers.GetBuild(), p.health) - } - - if p.later.health != nil { - p.health = p.later.health - } - - p.later = nil - } - - var cpt = make([]statusComponent, 0) - - for _, part := range p.cpt { - h := part.health - if part.later != nil { - - if part.later.health != nil { - h = part.later.health - } - - if part.later.info != nil { - name, release, build := part.later.info() - ok, ko := part.later.msgItm() - part = statusComponent{ - statusItem: newItem(name, ok, ko, release, build, h), - reference: part.reference, - WarnIfErr: part.WarnIfErr, - later: nil, - } - } else if p.later.version != nil { - v := p.later.version() - ok, ko := p.later.msgItm() - - part = statusComponent{ - statusItem: newItem(v.GetPackage(), ok, ko, v.GetRelease(), v.GetBuild(), h), - reference: part.reference, - WarnIfErr: part.WarnIfErr, - later: nil, - } - } - } - - cpt = append(cpt, part) - } - - p.init = true - p.cpt = cpt -} - -func (p *mainPackage) cleanPrefix(prefix string) string { - return path.Clean(strings.TrimRight(path.Join("/", prefix), "/")) -} - -func (p *mainPackage) Register(prefix string, register router.RegisterRouter) { - prefix = p.cleanPrefix(prefix) - - register(http.MethodGet, prefix, p.header, p.Get) - - if prefix != "/" { - register(http.MethodGet, prefix+"/", p.header, p.Get) - } -} - -func (p *mainPackage) RegisterGroup(group, prefix string, register router.RegisterRouterInGroup) { - prefix = p.cleanPrefix(prefix) - - register(group, http.MethodGet, prefix, p.header, p.Get) - - if prefix != "/" { - register(group, http.MethodGet, prefix+"/", p.header, p.Get) - } -} - -func (p *statusItem) GetStatusResponse(c *gin.Context) StatusItemResponse { - res := StatusItemResponse{ - Name: p.name, - Status: statusOK, - Message: p.msgOK, - Release: p.release, - HashBuild: p.build, - } - - if p.health != nil { - if err := p.health(); err != nil { - msg := fmt.Sprintf("%s: %v", p.msgKO, err) - c.Errors = append(c.Errors, &gin.Error{ - //nolint #goerr113 - Err: fmt.Errorf(msg), - Type: gin.ErrorTypePrivate, - }) - res = StatusItemResponse{ - Name: p.name, - Status: statusKO, - Message: msg, - Release: p.release, - HashBuild: p.build, - } - } - } - - return res -} - -func (p *mainPackage) Get(c *gin.Context) { - if !p.init { - p.initStatus() - } - - hasError := false - res := StatusResponse{ - p.GetStatusResponse(c), - make([]StatusItemResponse, 0), - } - - sem := semaphore.NewSemaphore(0) - defer func() { - if sem != nil { - sem.DeferMain() - } - }() - - for _, pkg := range p.cpt { - _ = sem.NewWorker() - - go func() { - defer sem.DeferWorker() - - pres := pkg.GetStatusResponse(c) - res.Component = append(res.Component, pres) - }() - } - - _ = sem.WaitAll() - - for _, pres := range res.Component { - if res.Status == statusOK && pres.Status == statusKO { - for _, pkg := range p.cpt { - if pkg.name == pres.Name && pkg.WarnIfErr { - res.Status = statusKO - } - } - } - - if !hasError && pres.Status == statusKO { - hasError = true - } - } - - if res.Status != statusOK { - if res.Message == p.msgOK { - res.Message = p.msgCptErr - } else if res.Message != p.msgKO { - res.Message = strings.Join([]string{res.Message, p.msgCptErr}, ", ") - } - - c.AbortWithStatusJSON(p.codeKO, &res) - } else if hasError { - c.JSON(p.codeCpt, &res) - } else { - c.JSON(p.codeOK, &res) - } -} - -func (p *mainPackage) SetErrorCode(codeOk, codeKO, codeCptNoMandatoryKO int) { - p.codeOK = codeOk - p.codeKO = codeKO - p.codeCpt = codeCptNoMandatoryKO -}