Skip to content

Commit

Permalink
[#79]: feature: simplify health/readiness checks
Browse files Browse the repository at this point in the history
  • Loading branch information
rustatian authored Sep 16, 2024
2 parents 1b9c916 + 8546b5e commit dc69b41
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 127 deletions.
2 changes: 0 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@ linters: # All available linters list: <https://golangci-lint.run/usage/linters/
- copyloopvar # checks for pointers to enclosing loop variables
- gochecknoglobals # Checks that no globals are present in Go code
- gochecknoinits # Checks that no init functions are present in Go code
- gocognit # Computes and checks the cognitive complexity of functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # The most opinionated Go source code linter
- gocyclo # Computes and checks the cyclomatic complexity of functions
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
- revive
Expand Down
19 changes: 19 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package status

type Report struct {
PluginName string `json:"plugin_name"`
ErrorMessage string `json:"error_message"`
StatusCode int `json:"status_code"`
}

type JobsReport struct {
Pipeline string `json:"pipeline"`
Priority uint64 `json:"priority"`
Ready bool `json:"ready"`
Queue string `json:"queue"`
Active int64 `json:"active"`
Delayed int64 `json:"delayed"`
Reserved int64 `json:"reserved"`
Driver string `json:"driver"`
ErrorMessage string `json:"error_message"`
}
3 changes: 2 additions & 1 deletion go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/roadrunner-server/jobs/v5 v5.0.4 h1:bRJpbVfTlkJraFattpmAu3Xg1hHwkcrDpgdCt91jAZc=
github.com/roadrunner-server/jobs/v5 v5.0.4/go.mod h1:me+Aaf45VQFg0GhuUQjZYUIY9MqhGB2MLy2A87WQ1lo=
github.com/roadrunner-server/status/v4 v4.4.15 h1:AAjCR/ofxxxIE7JdXRXV0jliaQM9ntSMdpgrqJH8MRk=
github.com/roadrunner-server/status/v4 v4.4.15/go.mod h1:hWKyeEfbohz8XqLlrD4+GXgna4lBPUzto+SVYP5XKOY=
github.com/sagikazarmark/crypt v0.10.0 h1:96E1qrToLBU6fGzo+PRRz7KGOc9FkYFiPnR3/zf8Smg=
Expand Down Expand Up @@ -293,7 +295,6 @@ golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
Expand Down
163 changes: 125 additions & 38 deletions health.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package status

import (
"fmt"
"html"
"encoding/json"
"net/http"
"sync/atomic"

Expand Down Expand Up @@ -31,58 +30,146 @@ func (rd *Health) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

if r == nil || r.URL == nil || r.URL.Query() == nil {
http.Error(
w,
"No plugins provided in query. Query should be in form of: health?plugin=plugin1&plugin=plugin2",
http.StatusBadRequest,
)
return
}
// report will be used either for all plugins or for the Plugins in the query
report := make([]*Report, 0, 2)

plg := r.URL.Query()[pluginsQuery]
// if no Plugins provided, check them all
if len(plg) == 0 {
rd.log.Debug("no plugins provided, checking all plugins")

for k, pl := range rd.statusRegistry {
if pl == nil {
report = append(report, &Report{
PluginName: k,
ErrorMessage: "plugin is nil or not initialized",
StatusCode: http.StatusNotFound,
})

rd.log.Info("plugin is nil or not initialized", zap.String("plugin", k))
continue
}

st, err := pl.Status()
if err != nil {
w.WriteHeader(rd.unavailableStatusCode)
report = append(report, &Report{
PluginName: k,
ErrorMessage: err.Error(),
StatusCode: rd.unavailableStatusCode,
})
continue
}

if st == nil {
report = append(report, &Report{
PluginName: k,
ErrorMessage: "plugin is not available, returned nil",
StatusCode: rd.unavailableStatusCode,
})
continue
}

pl := r.URL.Query()[pluginsQuery]
switch {
case st.Code >= 500:
w.WriteHeader(rd.unavailableStatusCode)

report = append(report, &Report{
PluginName: k,
ErrorMessage: "internal server error, see logs",
StatusCode: rd.unavailableStatusCode,
})
case st.Code >= 100 && st.Code <= 400:
report = append(report, &Report{
PluginName: k,
StatusCode: st.Code,
})
default:
report = append(report, &Report{
PluginName: k,
ErrorMessage: "unexpected status code",
StatusCode: st.Code,
})
}
}

data, err := json.Marshal(report)
if err != nil {
// TODO do we need to write this error to the ResponseWriter?
rd.log.Error("failed to marshal response", zap.Error(err))
return
}
// write the response
_, err = w.Write(data)
if err != nil {
rd.log.Error("failed to write response", zap.Error(err))
}

if len(pl) == 0 {
http.Error(
w,
"No plugins provided in query. Query should be in form of: health?plugin=plugin1&plugin=plugin2",
http.StatusBadRequest,
)
return
}

// iterate over all provided plugins
for i := 0; i < len(pl); i++ {
switch {
// check workers for the plugin
case rd.statusRegistry[pl[i]] != nil:
st, errS := rd.statusRegistry[pl[i]].Status()
if errS != nil {
http.Error(w, errS.Error(), rd.unavailableStatusCode)
return
// iterate over all provided Plugins
for i := 0; i < len(plg); i++ {
if svc, ok := rd.statusRegistry[plg[i]]; ok {
if svc == nil {
continue
}

st, err := rd.statusRegistry[plg[i]].Status()
if err != nil {
report = append(report, &Report{
PluginName: plg[i],
ErrorMessage: err.Error(),
StatusCode: http.StatusInternalServerError,
})

continue
}

if st == nil {
w.WriteHeader(rd.unavailableStatusCode)
// nil can be only if the service unavailable
_, _ = w.Write([]byte(fmt.Sprintf(template, html.EscapeString(pl[i]), rd.unavailableStatusCode)))
return
report = append(report, &Report{
PluginName: plg[i],
ErrorMessage: "plugin is not available",
StatusCode: rd.unavailableStatusCode,
})

continue
}

switch {
case st.Code >= 500:
// on >=500, write header, because it'll be written on Write (200)
w.WriteHeader(rd.unavailableStatusCode)
// if there is 500 or 503 status code return immediately
_, _ = w.Write([]byte(fmt.Sprintf(template, html.EscapeString(pl[i]), rd.unavailableStatusCode)))
return
report = append(report, &Report{
PluginName: plg[i],
ErrorMessage: "internal server error, see logs",
StatusCode: rd.unavailableStatusCode,
})
case st.Code >= 100 && st.Code <= 400:
_, _ = w.Write([]byte(fmt.Sprintf(template, html.EscapeString(pl[i]), st.Code)))
continue
report = append(report, &Report{
PluginName: plg[i],
StatusCode: st.Code,
})
default:
_, _ = w.Write([]byte(fmt.Sprintf("plugin: %s not found", html.EscapeString(pl[i]))))
report = append(report, &Report{
PluginName: plg[i],
ErrorMessage: "unexpected status code",
StatusCode: st.Code,
})
}
default:
_, _ = w.Write([]byte(fmt.Sprintf("plugin: %s not found", html.EscapeString(pl[i]))))
} else {
rd.log.Info("plugin does not support health checks", zap.String("plugin", plg[i]))
}
}

data, err := json.Marshal(report)
if err != nil {
rd.log.Error("failed to marshal response", zap.Error(err))
}

// write the response
_, err = w.Write(data)
if err != nil {
rd.log.Error("failed to write response", zap.Error(err))
}
}
43 changes: 25 additions & 18 deletions jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@ package status

import (
"context"
"fmt"
"html"
"encoding/json"
"net/http"
"sync/atomic"

"go.uber.org/zap"
)

const (
jobsTemplate string = "plugin: %s: pipeline: %s | priority: %d | ready: %t | queue: %s | active: %d | delayed: %d | reserved: %d | driver: %s | error: %s \n"
)

type Jobs struct {
statusJobsRegistry JobsChecker
unavailableStatusCode int
Expand Down Expand Up @@ -47,19 +42,31 @@ func (jb *Jobs) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
return
}

report := make([]*JobsReport, 0, len(jobStates))

// write info about underlying drivers
for i := 0; i < len(jobStates); i++ {
_, _ = w.Write([]byte(html.EscapeString(fmt.Sprintf(jobsTemplate,
"jobs", // only JOBS plugin
jobStates[i].Pipeline,
jobStates[i].Priority,
jobStates[i].Ready,
jobStates[i].Queue,
jobStates[i].Active,
jobStates[i].Delayed,
jobStates[i].Reserved,
jobStates[i].Driver,
jobStates[i].ErrorMessage,
))))
report = append(report, &JobsReport{
Pipeline: jobStates[i].Pipeline,
Priority: jobStates[i].Priority,
Ready: jobStates[i].Ready,
Queue: jobStates[i].Queue,
Active: jobStates[i].Active,
Delayed: jobStates[i].Delayed,
Reserved: jobStates[i].Reserved,
Driver: jobStates[i].Driver,
ErrorMessage: jobStates[i].ErrorMessage,
})
}

data, err := json.Marshal(report)
if err != nil {
jb.log.Error("failed to marshal jobs state report", zap.Error(err))
return
}

_, err = w.Write(data)
if err != nil {
jb.log.Error("failed to write jobs state report", zap.Error(err))
}
}
1 change: 0 additions & 1 deletion plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const (
// PluginName declares public plugin name.
PluginName = "status"
pluginsQuery string = "plugin"
template string = "plugin: %s, status: %d\n"
)

type Configurer interface {
Expand Down
Loading

0 comments on commit dc69b41

Please sign in to comment.