chore: cleanup, add docs, refactor some quirks and prepare release
This commit is contained in:
parent
a51563c53d
commit
0faff1d481
18 changed files with 366 additions and 124 deletions
|
@ -16,19 +16,24 @@ jobs:
|
|||
fetch-depth: '0'
|
||||
lfs: 'true'
|
||||
fetch-tags: 'true'
|
||||
|
||||
- name: Setup Go 1.21.x
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
# Semantic version range syntax or exact version of Go
|
||||
go-version: '1.21.x'
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
go install gotest.tools/gotestsum@latest
|
||||
gotestsum --junitfile out/results.xml --format pkgname-and-test-fails -- -race -shuffle=on ./...
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v5
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
with:
|
||||
|
@ -36,3 +41,9 @@ jobs:
|
|||
version: latest
|
||||
args: release --clean --snapshot
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v5
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- id: nurse
|
||||
binary: nurse
|
||||
|
@ -26,6 +23,16 @@ changelog:
|
|||
- '^docs:'
|
||||
- '^test:'
|
||||
|
||||
release:
|
||||
gitea:
|
||||
owner: prskr
|
||||
repo: nurse
|
||||
ids:
|
||||
- nurse
|
||||
mode: replace
|
||||
extra_files:
|
||||
- glob: ./nurse.yaml
|
||||
|
||||
dockers:
|
||||
- ids:
|
||||
- nurse
|
||||
|
|
88
README.md
88
README.md
|
@ -1,3 +1,89 @@
|
|||
# Nurse
|
||||
|
||||
A generic service health sidecar
|
||||
## Usage
|
||||
|
||||
Nurse comes currently with 2 different operation modes:
|
||||
|
||||
- server
|
||||
- CLI
|
||||
|
||||
The server starts an HTTP server with configurable endpoints which you can use e.g. in Kubernetes environments to
|
||||
distinguish between:
|
||||
|
||||
- startup
|
||||
- readiness
|
||||
- liveness
|
||||
|
||||
probes.
|
||||
Every endpoint has a distinguished set of checks that are executed when you hit the endpoint.
|
||||
Currently, there is no caching in place (and there are also no plans to change that).
|
||||
|
||||
The CLI operation mode on the other hand executes all checks that are provided as arguments e.g. in Docker Swarm
|
||||
environment where the container image has to ship the health check CLI.
|
||||
|
||||
### Primer about checks
|
||||
|
||||
All checks are executed **in parallel** which means you shouldn't rely on a certain execution
|
||||
|
||||
### Global config/options
|
||||
|
||||
Nurse comes with the following global options:
|
||||
|
||||
| Switch | Environment variable | Default value | Description |
|
||||
|--------------------|------------------------|--------------------------------------------------------------|---------------------------------------------------------------------|
|
||||
| `--config` | `NURSE_CONFIG` | `$HOME/.nurse.yaml`, `/etc/nurse/config.yaml`,`./nurse.yaml` | path to the config file |
|
||||
| `--check-timeout` | `NURSE_CHECK_TIMEOUT` | `500ms` | Timeout for executing all checks |
|
||||
| `--check-attempts` | `NURSE_CHECK_ATTEMPTS` | `20` | How often checks should be retried before they're considered failed |
|
||||
| `--log.level` | | `info` | Default log level |
|
||||
| `--servers` | `NURSE_SERVER_<name>` | | Configure server URLs via environment variables |
|
||||
|
||||
The individual sub-commands come with additional options, like for example configuring endpoints via environment
|
||||
variables as well.
|
||||
|
||||
The [nurse.yaml](./nurse.yaml) describes how to configure Nurse via a configuration file.
|
||||
|
||||
The most interesting root nodes are:
|
||||
|
||||
- servers
|
||||
- endpoints
|
||||
|
||||
Within `servers` you can configure different servers for further usage in checks.
|
||||
For example, to configure a Redis server: `redis://localhost:6379/0`.
|
||||
Depending on the individual protocols there are further configuration options.
|
||||
|
||||
Within `endpoints` you can configure different HTTP endpoints the server exposes and which checks should be executed for
|
||||
which endpoint.
|
||||
|
||||
### Server
|
||||
|
||||
The `server` sub-command comes with the following additional config options:
|
||||
|
||||
| Switch | Environment variable | Default value | Description |
|
||||
|-----------------------------|----------------------------------|---------------|------------------------------------------------------------|
|
||||
| `--endpoints` | `NURSE_ENDPOINT_<name>` | | Configure HTTP endpoints via environment variables |
|
||||
| `--http.address` | `NURSE_HTTP_ADDRESS` | `:8080` | IP and port the server will be listening on |
|
||||
| `--http.read-header-timout` | `NURSE_HTTP_READ_HEADER_TIMEOUT` | `100ms` | Timeout until when the client has to have sent the headers |
|
||||
|
||||
To configure an endpoint via an environment variable, set it like this:
|
||||
|
||||
```
|
||||
NURSE_ENDPOINT_HEALTHZ='http.GET("https://api.chucknorris.io/jokes/random")=>Status(200);redis.PING("local-redis")'
|
||||
```
|
||||
|
||||
The server will print the configured routes when it is starting up.
|
||||
In the aforementioned case you should see something like:
|
||||
|
||||
```
|
||||
{"time":"xxxxx","level":"INFO","msg":"Configuring route","route":"/healthz"}
|
||||
```
|
||||
|
||||
Multiple checks can be configured by separating them with a `;` into multiple 'expressions'.
|
||||
|
||||
### CLI
|
||||
|
||||
The CLI has no additional config options compared to the server.
|
||||
It simply takes all arguments you pass to it, tries to parse them as checks and executes them with the given time limit.
|
||||
If one of the check fails it will exit with a non-zero exit code.
|
||||
|
||||
Multiple checks can either be passed as single argument in `''` separated with a `;` just like in the environment variables, or you can pass multiple arguments.
|
||||
The result will be the same.
|
|
@ -3,6 +3,7 @@ package check
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"code.icb4dc0.de/prskr/nurse/config"
|
||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||
|
@ -21,7 +22,8 @@ type (
|
|||
|
||||
Context interface {
|
||||
context.Context
|
||||
AttemptContext() (context.Context, context.CancelFunc)
|
||||
AttemptCount() uint
|
||||
AttemptTimeout() time.Duration
|
||||
WithParent(ctx context.Context) Context
|
||||
}
|
||||
|
||||
|
|
|
@ -14,21 +14,27 @@ func AttemptsContext(parent context.Context, numberOfAttempts uint, attemptTimeo
|
|||
return &checkContext{
|
||||
Context: base,
|
||||
attemptTimeout: attemptTimeout,
|
||||
numberOfAttempts: numberOfAttempts,
|
||||
}, cancel
|
||||
}
|
||||
|
||||
type checkContext struct {
|
||||
attemptTimeout time.Duration
|
||||
numberOfAttempts uint
|
||||
context.Context
|
||||
}
|
||||
|
||||
func (c *checkContext) AttemptCount() uint {
|
||||
return c.numberOfAttempts
|
||||
}
|
||||
|
||||
func (c *checkContext) AttemptTimeout() time.Duration {
|
||||
return c.attemptTimeout
|
||||
}
|
||||
|
||||
func (c *checkContext) WithParent(ctx context.Context) Context {
|
||||
return &checkContext{
|
||||
Context: ctx,
|
||||
attemptTimeout: c.attemptTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *checkContext) AttemptContext() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(c, c.attemptTimeout)
|
||||
}
|
||||
|
|
47
cmd/app.go
47
cmd/app.go
|
@ -6,12 +6,13 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"code.icb4dc0.de/prskr/nurse/check"
|
||||
"code.icb4dc0.de/prskr/nurse/config"
|
||||
"code.icb4dc0.de/prskr/nurse/protocols/http"
|
||||
"code.icb4dc0.de/prskr/nurse/protocols/redis"
|
||||
"code.icb4dc0.de/prskr/nurse/protocols/sql"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -19,6 +20,16 @@ const (
|
|||
defaultAttemptCount = 20
|
||||
)
|
||||
|
||||
const (
|
||||
logLevelFlag = "log.level"
|
||||
httpAddressFlag = "http.address"
|
||||
httpReadHeaderTimeout = "http.read-header-timeout"
|
||||
maxCheckAttemptsFlag = "check-attempts"
|
||||
checkTimeoutFlag = "check-timeout"
|
||||
serversFlag = "servers"
|
||||
configFlag = "config"
|
||||
)
|
||||
|
||||
func NewApp() (*cli.App, error) {
|
||||
app := &app{
|
||||
registry: check.NewRegistry(),
|
||||
|
@ -46,30 +57,30 @@ func NewApp() (*cli.App, error) {
|
|||
Before: app.init,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Name: configFlag,
|
||||
Usage: "Config file to load, if not set `$HOME/.nurse.yaml`, `/etc/nurse/config.yaml` and `./nurse.yaml` are tried - optional",
|
||||
Aliases: []string{"c"},
|
||||
EnvVars: []string{"NURSE_CONFIG"},
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: "check-timeout",
|
||||
Name: checkTimeoutFlag,
|
||||
Usage: "Timeout when running checks",
|
||||
Value: defaultCheckTimeout,
|
||||
EnvVars: []string{"NURSE_CHECK_TIMEOUT"},
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "check-attempts",
|
||||
Name: maxCheckAttemptsFlag,
|
||||
Usage: "Number of attempts for a check",
|
||||
Value: defaultAttemptCount,
|
||||
EnvVars: []string{"NURSE_CHECK_ATTEMPTS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Name: logLevelFlag,
|
||||
Usage: "Log level to use",
|
||||
Value: "info",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "servers",
|
||||
Name: serversFlag,
|
||||
Usage: "",
|
||||
Aliases: []string{"s"},
|
||||
},
|
||||
|
@ -83,9 +94,21 @@ func NewApp() (*cli.App, error) {
|
|||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "endpoints",
|
||||
Usage: "",
|
||||
Usage: "Endpoints to expose in the HTTP server",
|
||||
Aliases: []string{"ep"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: httpAddressFlag,
|
||||
Usage: "HTTP server address",
|
||||
Value: ":8080",
|
||||
EnvVars: []string{"NURSE_HTTP_ADDRESS"},
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: httpReadHeaderTimeout,
|
||||
Usage: "Timeout for reading headers in the HTTP server",
|
||||
Value: 100 * time.Millisecond,
|
||||
EnvVars: []string{"NURSE_HTTP_READ_HEADER_TIMEOUT"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -109,16 +132,16 @@ type app struct {
|
|||
}
|
||||
|
||||
func (a *app) init(ctx *cli.Context) (err error) {
|
||||
if err = a.configureLogging(ctx.String("log-level")); err != nil {
|
||||
if err = a.configureLogging(ctx.String(logLevelFlag)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.nurseInstance, err = config.New(
|
||||
config.WithCheckAttempts(ctx.Uint("check-attempts")),
|
||||
config.WithCheckDuration(ctx.Duration("check-timeout")),
|
||||
config.WithConfigFile(ctx.String("config")),
|
||||
config.WithCheckAttempts(ctx.Uint(maxCheckAttemptsFlag)),
|
||||
config.WithCheckDuration(ctx.Duration(checkTimeoutFlag)),
|
||||
config.WithConfigFile(ctx.String(configFlag)),
|
||||
config.WithServersFromEnv(),
|
||||
config.WithServersFromArgs(ctx.StringSlice("servers")),
|
||||
config.WithServersFromArgs(ctx.StringSlice(serversFlag)),
|
||||
config.WithEndpointsFromEnv(),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"code.icb4dc0.de/prskr/nurse/api"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
|
@ -23,9 +25,12 @@ func (a *server) RunServer(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
srv := http.Server{
|
||||
Addr: ":8080",
|
||||
Addr: ctx.String(httpAddressFlag),
|
||||
ReadHeaderTimeout: ctx.Duration(httpReadHeaderTimeout),
|
||||
BaseContext: func(listener net.Listener) context.Context {
|
||||
return ctx.Context
|
||||
},
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 100 * time.Millisecond,
|
||||
}
|
||||
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
|
|
2
go.mod
2
go.mod
|
@ -17,7 +17,6 @@ require (
|
|||
github.com/testcontainers/testcontainers-go v0.26.0
|
||||
github.com/urfave/cli/v2 v2.26.0
|
||||
github.com/valyala/bytebufferpool v1.0.0
|
||||
golang.org/x/exp v0.0.0-20231127185646-65229373498e
|
||||
golang.org/x/sync v0.5.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
@ -70,6 +69,7 @@ require (
|
|||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
golang.org/x/crypto v0.16.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -172,8 +172,6 @@ github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0h
|
|||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
|
||||
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
|
|
42
internal/retry/retry.go
Normal file
42
internal/retry/retry.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package retry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Retry executes a function with the given number of attempts and attempt timeouts.
|
||||
// It returns the last error encountered during the attempts.
|
||||
// If the context is canceled, it returns the context error (if there is no previous error),
|
||||
// or the joined error of the last error and the context error (otherwise).
|
||||
func Retry(ctx context.Context, numberOfAttempts uint, attemptTimeout time.Duration, f func(ctx context.Context, attempt int) error) (lastErr error) {
|
||||
baseCtx, baseCancel := context.WithTimeout(ctx, time.Duration(numberOfAttempts)*attemptTimeout)
|
||||
defer baseCancel()
|
||||
|
||||
for i := uint(0); i < numberOfAttempts; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if lastErr == nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return errors.Join(lastErr, ctx.Err())
|
||||
default:
|
||||
attemptCtx, attemptCancel := context.WithTimeout(baseCtx, attemptTimeout)
|
||||
|
||||
lastErr = f(attemptCtx, int(i))
|
||||
if lastErr == nil {
|
||||
attemptCancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
if attemptCtx.Err() == nil {
|
||||
<-attemptCtx.Done()
|
||||
}
|
||||
|
||||
attemptCancel()
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
|
@ -2,6 +2,7 @@ package http
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
@ -9,6 +10,7 @@ import (
|
|||
"code.icb4dc0.de/prskr/nurse/check"
|
||||
"code.icb4dc0.de/prskr/nurse/config"
|
||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||
"code.icb4dc0.de/prskr/nurse/internal/retry"
|
||||
"code.icb4dc0.de/prskr/nurse/validation"
|
||||
)
|
||||
|
||||
|
@ -38,12 +40,15 @@ func (g *GenericCheck) SetClient(client *http.Client) {
|
|||
}
|
||||
|
||||
func (g *GenericCheck) Execute(ctx check.Context) error {
|
||||
slog.Default().Debug("Execute check",
|
||||
logger := slog.Default().With(
|
||||
slog.String("check", "http"),
|
||||
slog.String("method", g.Method),
|
||||
slog.String("url", g.URL),
|
||||
)
|
||||
|
||||
return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error {
|
||||
logger.Debug("Execute check", slog.Int("attempt", attempt))
|
||||
|
||||
var body io.Reader
|
||||
if len(g.Body) > 0 {
|
||||
body = bytes.NewReader(g.Body)
|
||||
|
@ -63,6 +68,7 @@ func (g *GenericCheck) Execute(ctx check.Context) error {
|
|||
}()
|
||||
|
||||
return g.validators.Validate(resp)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GenericCheck) UnmarshalCheck(c grammar.Check, _ config.ServerLookup) error {
|
||||
|
|
|
@ -13,6 +13,9 @@ func Module() *check.Module {
|
|||
check.WithCheck("get", check.FactoryFunc(func() check.SystemChecker {
|
||||
return new(GetCheck)
|
||||
})),
|
||||
check.WithCheck("set", check.FactoryFunc(func() check.SystemChecker {
|
||||
return new(SetCheck)
|
||||
})),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
|
@ -56,6 +56,11 @@ func TestChecks_Execute(t *testing.T) {
|
|||
check: `redis.PING("%s", "Hello, Redis!")`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "SET check",
|
||||
check: `redis.SET("%s", "Hello", "World!")`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"code.icb4dc0.de/prskr/nurse/internal/retry"
|
||||
|
||||
"code.icb4dc0.de/prskr/nurse/check"
|
||||
"code.icb4dc0.de/prskr/nurse/config"
|
||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||
|
@ -21,27 +23,14 @@ type GetCheck struct {
|
|||
}
|
||||
|
||||
func (g *GetCheck) Execute(ctx check.Context) error {
|
||||
slog.Default().Debug("Execute check",
|
||||
logger := slog.Default().With(
|
||||
slog.String("check", "redis.GET"),
|
||||
slog.String("key", g.Key),
|
||||
)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
attemptCtx, cancel := ctx.AttemptContext()
|
||||
err := g.executeAttempt(attemptCtx)
|
||||
cancel()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error {
|
||||
logger.Debug("Execute check", slog.Int("attempt", attempt))
|
||||
|
||||
func (g *GetCheck) executeAttempt(ctx context.Context) error {
|
||||
cmd := g.Get(ctx, g.Key)
|
||||
|
||||
if err := cmd.Err(); err != nil {
|
||||
|
@ -49,6 +38,7 @@ func (g *GetCheck) executeAttempt(ctx context.Context) error {
|
|||
}
|
||||
|
||||
return g.validators.Validate(cmd)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GetCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) error {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"code.icb4dc0.de/prskr/nurse/check"
|
||||
"code.icb4dc0.de/prskr/nurse/config"
|
||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||
"code.icb4dc0.de/prskr/nurse/internal/retry"
|
||||
"code.icb4dc0.de/prskr/nurse/validation"
|
||||
)
|
||||
|
||||
|
@ -21,32 +22,19 @@ type PingCheck struct {
|
|||
Message string
|
||||
}
|
||||
|
||||
func (p PingCheck) Execute(ctx check.Context) error {
|
||||
slog.Default().Debug("Execute check",
|
||||
func (p *PingCheck) Execute(ctx check.Context) error {
|
||||
logger := slog.Default().With(
|
||||
slog.String("check", "redis.PING"),
|
||||
slog.String("msg", p.Message),
|
||||
)
|
||||
|
||||
return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error {
|
||||
logger.Debug("Execute check", slog.Int("attempt", attempt))
|
||||
|
||||
if p.Message == "" {
|
||||
return p.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
attemptCtx, cancel := ctx.AttemptContext()
|
||||
err := p.executeAttempt(attemptCtx)
|
||||
cancel()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p PingCheck) executeAttempt(ctx context.Context) error {
|
||||
if resp, err := p.Do(ctx, "PING", p.Message).Text(); err != nil {
|
||||
return err
|
||||
} else if resp != p.Message {
|
||||
|
@ -54,6 +42,7 @@ func (p PingCheck) executeAttempt(ctx context.Context) error {
|
|||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *PingCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) error {
|
||||
|
|
69
protocols/redis/set.go
Normal file
69
protocols/redis/set.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"code.icb4dc0.de/prskr/nurse/check"
|
||||
"code.icb4dc0.de/prskr/nurse/config"
|
||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||
"code.icb4dc0.de/prskr/nurse/internal/retry"
|
||||
"code.icb4dc0.de/prskr/nurse/validation"
|
||||
)
|
||||
|
||||
var _ check.SystemChecker = (*SetCheck)(nil)
|
||||
|
||||
type SetCheck struct {
|
||||
redis.UniversalClient
|
||||
validators validation.Validator[redis.Cmder]
|
||||
Key, Value string
|
||||
}
|
||||
|
||||
func (s *SetCheck) Execute(ctx check.Context) error {
|
||||
logger := slog.Default().With(
|
||||
slog.String("check", "redis.SET"),
|
||||
slog.String("key", s.Key),
|
||||
slog.String("value", s.Value),
|
||||
)
|
||||
|
||||
return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error {
|
||||
logger.Debug("Execute check", slog.Int("attempt", attempt))
|
||||
|
||||
cmd := s.Set(ctx, s.Key, s.Value, -1)
|
||||
|
||||
if err := cmd.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.validators.Validate(cmd)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SetCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) error {
|
||||
const serverKeyAndValueArgsNumber = 3
|
||||
inst := c.Initiator
|
||||
if err := grammar.ValidateParameterCount(inst.Params, serverKeyAndValueArgsNumber); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
if s.UniversalClient, err = clientFromParam(inst.Params[0], lookup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Key, err = inst.Params[1].AsString(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Value, err = inst.Params[2].AsString(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.validators, err = registry.ValidatorsForFilters(c.Validators); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -75,12 +75,22 @@ func (g *GenericCmdValidator) Validate(cmder redis.Cmder) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if in, ok := cmder.(*redis.StringCmd); ok {
|
||||
switch in := cmder.(type) {
|
||||
case *redis.StringCmd:
|
||||
if err := in.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := in.Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.comparator.Equals(res)
|
||||
case *redis.StatusCmd:
|
||||
if err := in.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"code.icb4dc0.de/prskr/nurse/check"
|
||||
"code.icb4dc0.de/prskr/nurse/config"
|
||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||
"code.icb4dc0.de/prskr/nurse/internal/retry"
|
||||
"code.icb4dc0.de/prskr/nurse/validation"
|
||||
)
|
||||
|
||||
|
@ -44,29 +45,17 @@ func (s *SelectCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup
|
|||
}
|
||||
|
||||
func (s *SelectCheck) Execute(ctx check.Context) error {
|
||||
slog.Default().Debug("Execute check",
|
||||
logger := slog.Default().With(
|
||||
slog.String("check", "sql.SELECT"),
|
||||
slog.String("query", s.Query),
|
||||
)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
attemptCtx, cancel := ctx.AttemptContext()
|
||||
err := s.executeAttempt(attemptCtx)
|
||||
cancel()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error {
|
||||
logger.Debug("Execute check", slog.Int("attempt", attempt))
|
||||
|
||||
func (s *SelectCheck) executeAttempt(ctx context.Context) (err error) {
|
||||
var rows *sql.Rows
|
||||
rows, err = s.QueryContext(ctx, s.Query)
|
||||
logger.Debug("")
|
||||
|
||||
rows, err := s.QueryContext(ctx, s.Query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -76,4 +65,5 @@ func (s *SelectCheck) executeAttempt(ctx context.Context) (err error) {
|
|||
}()
|
||||
|
||||
return s.validators.Validate(rows)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue