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'
|
fetch-depth: '0'
|
||||||
lfs: 'true'
|
lfs: 'true'
|
||||||
fetch-tags: 'true'
|
fetch-tags: 'true'
|
||||||
|
|
||||||
- name: Setup Go 1.21.x
|
- name: Setup Go 1.21.x
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
# Semantic version range syntax or exact version of Go
|
# Semantic version range syntax or exact version of Go
|
||||||
go-version: '1.21.x'
|
go-version: '1.21.x'
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
uses: arduino/setup-task@v1
|
uses: arduino/setup-task@v1
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
go install gotest.tools/gotestsum@latest
|
go install gotest.tools/gotestsum@latest
|
||||||
gotestsum --junitfile out/results.xml --format pkgname-and-test-fails -- -race -shuffle=on ./...
|
gotestsum --junitfile out/results.xml --format pkgname-and-test-fails -- -race -shuffle=on ./...
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@v5
|
- uses: goreleaser/goreleaser-action@v5
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
with:
|
with:
|
||||||
|
@ -36,3 +41,9 @@ jobs:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean --snapshot
|
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:
|
builds:
|
||||||
- id: nurse
|
- id: nurse
|
||||||
binary: nurse
|
binary: nurse
|
||||||
|
@ -26,6 +23,16 @@ changelog:
|
||||||
- '^docs:'
|
- '^docs:'
|
||||||
- '^test:'
|
- '^test:'
|
||||||
|
|
||||||
|
release:
|
||||||
|
gitea:
|
||||||
|
owner: prskr
|
||||||
|
repo: nurse
|
||||||
|
ids:
|
||||||
|
- nurse
|
||||||
|
mode: replace
|
||||||
|
extra_files:
|
||||||
|
- glob: ./nurse.yaml
|
||||||
|
|
||||||
dockers:
|
dockers:
|
||||||
- ids:
|
- ids:
|
||||||
- nurse
|
- nurse
|
||||||
|
|
88
README.md
88
README.md
|
@ -1,3 +1,89 @@
|
||||||
# Nurse
|
# 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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.icb4dc0.de/prskr/nurse/config"
|
"code.icb4dc0.de/prskr/nurse/config"
|
||||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||||
|
@ -21,7 +22,8 @@ type (
|
||||||
|
|
||||||
Context interface {
|
Context interface {
|
||||||
context.Context
|
context.Context
|
||||||
AttemptContext() (context.Context, context.CancelFunc)
|
AttemptCount() uint
|
||||||
|
AttemptTimeout() time.Duration
|
||||||
WithParent(ctx context.Context) Context
|
WithParent(ctx context.Context) Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,23 +12,29 @@ func AttemptsContext(parent context.Context, numberOfAttempts uint, attemptTimeo
|
||||||
base, cancel := context.WithTimeout(parent, finalTimeout)
|
base, cancel := context.WithTimeout(parent, finalTimeout)
|
||||||
|
|
||||||
return &checkContext{
|
return &checkContext{
|
||||||
Context: base,
|
Context: base,
|
||||||
attemptTimeout: attemptTimeout,
|
attemptTimeout: attemptTimeout,
|
||||||
|
numberOfAttempts: numberOfAttempts,
|
||||||
}, cancel
|
}, cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
type checkContext struct {
|
type checkContext struct {
|
||||||
attemptTimeout time.Duration
|
attemptTimeout time.Duration
|
||||||
|
numberOfAttempts uint
|
||||||
context.Context
|
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 {
|
func (c *checkContext) WithParent(ctx context.Context) Context {
|
||||||
return &checkContext{
|
return &checkContext{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
attemptTimeout: c.attemptTimeout,
|
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"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"code.icb4dc0.de/prskr/nurse/check"
|
"code.icb4dc0.de/prskr/nurse/check"
|
||||||
"code.icb4dc0.de/prskr/nurse/config"
|
"code.icb4dc0.de/prskr/nurse/config"
|
||||||
"code.icb4dc0.de/prskr/nurse/protocols/http"
|
"code.icb4dc0.de/prskr/nurse/protocols/http"
|
||||||
"code.icb4dc0.de/prskr/nurse/protocols/redis"
|
"code.icb4dc0.de/prskr/nurse/protocols/redis"
|
||||||
"code.icb4dc0.de/prskr/nurse/protocols/sql"
|
"code.icb4dc0.de/prskr/nurse/protocols/sql"
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -19,6 +20,16 @@ const (
|
||||||
defaultAttemptCount = 20
|
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) {
|
func NewApp() (*cli.App, error) {
|
||||||
app := &app{
|
app := &app{
|
||||||
registry: check.NewRegistry(),
|
registry: check.NewRegistry(),
|
||||||
|
@ -46,30 +57,30 @@ func NewApp() (*cli.App, error) {
|
||||||
Before: app.init,
|
Before: app.init,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&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",
|
Usage: "Config file to load, if not set `$HOME/.nurse.yaml`, `/etc/nurse/config.yaml` and `./nurse.yaml` are tried - optional",
|
||||||
Aliases: []string{"c"},
|
Aliases: []string{"c"},
|
||||||
EnvVars: []string{"NURSE_CONFIG"},
|
EnvVars: []string{"NURSE_CONFIG"},
|
||||||
},
|
},
|
||||||
&cli.DurationFlag{
|
&cli.DurationFlag{
|
||||||
Name: "check-timeout",
|
Name: checkTimeoutFlag,
|
||||||
Usage: "Timeout when running checks",
|
Usage: "Timeout when running checks",
|
||||||
Value: defaultCheckTimeout,
|
Value: defaultCheckTimeout,
|
||||||
EnvVars: []string{"NURSE_CHECK_TIMEOUT"},
|
EnvVars: []string{"NURSE_CHECK_TIMEOUT"},
|
||||||
},
|
},
|
||||||
&cli.UintFlag{
|
&cli.UintFlag{
|
||||||
Name: "check-attempts",
|
Name: maxCheckAttemptsFlag,
|
||||||
Usage: "Number of attempts for a check",
|
Usage: "Number of attempts for a check",
|
||||||
Value: defaultAttemptCount,
|
Value: defaultAttemptCount,
|
||||||
EnvVars: []string{"NURSE_CHECK_ATTEMPTS"},
|
EnvVars: []string{"NURSE_CHECK_ATTEMPTS"},
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "log-level",
|
Name: logLevelFlag,
|
||||||
Usage: "Log level to use",
|
Usage: "Log level to use",
|
||||||
Value: "info",
|
Value: "info",
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "servers",
|
Name: serversFlag,
|
||||||
Usage: "",
|
Usage: "",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
},
|
},
|
||||||
|
@ -83,9 +94,21 @@ func NewApp() (*cli.App, error) {
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "endpoints",
|
Name: "endpoints",
|
||||||
Usage: "",
|
Usage: "Endpoints to expose in the HTTP server",
|
||||||
Aliases: []string{"ep"},
|
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) {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.nurseInstance, err = config.New(
|
a.nurseInstance, err = config.New(
|
||||||
config.WithCheckAttempts(ctx.Uint("check-attempts")),
|
config.WithCheckAttempts(ctx.Uint(maxCheckAttemptsFlag)),
|
||||||
config.WithCheckDuration(ctx.Duration("check-timeout")),
|
config.WithCheckDuration(ctx.Duration(checkTimeoutFlag)),
|
||||||
config.WithConfigFile(ctx.String("config")),
|
config.WithConfigFile(ctx.String(configFlag)),
|
||||||
config.WithServersFromEnv(),
|
config.WithServersFromEnv(),
|
||||||
config.WithServersFromArgs(ctx.StringSlice("servers")),
|
config.WithServersFromArgs(ctx.StringSlice(serversFlag)),
|
||||||
config.WithEndpointsFromEnv(),
|
config.WithEndpointsFromEnv(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"code.icb4dc0.de/prskr/nurse/api"
|
"code.icb4dc0.de/prskr/nurse/api"
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type server struct {
|
type server struct {
|
||||||
|
@ -23,9 +25,12 @@ func (a *server) RunServer(ctx *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := http.Server{
|
srv := http.Server{
|
||||||
Addr: ":8080",
|
Addr: ctx.String(httpAddressFlag),
|
||||||
Handler: mux,
|
ReadHeaderTimeout: ctx.Duration(httpReadHeaderTimeout),
|
||||||
ReadHeaderTimeout: 100 * time.Millisecond,
|
BaseContext: func(listener net.Listener) context.Context {
|
||||||
|
return ctx.Context
|
||||||
|
},
|
||||||
|
Handler: mux,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := srv.ListenAndServe(); err != nil {
|
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/testcontainers/testcontainers-go v0.26.0
|
||||||
github.com/urfave/cli/v2 v2.26.0
|
github.com/urfave/cli/v2 v2.26.0
|
||||||
github.com/valyala/bytebufferpool v1.0.0
|
github.com/valyala/bytebufferpool v1.0.0
|
||||||
golang.org/x/exp v0.0.0-20231127185646-65229373498e
|
|
||||||
golang.org/x/sync v0.5.0
|
golang.org/x/sync v0.5.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
@ -70,6 +69,7 @@ require (
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
golang.org/x/crypto v0.16.0 // 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/mod v0.14.0 // indirect
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.15.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 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
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 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 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
|
||||||
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
"code.icb4dc0.de/prskr/nurse/check"
|
"code.icb4dc0.de/prskr/nurse/check"
|
||||||
"code.icb4dc0.de/prskr/nurse/config"
|
"code.icb4dc0.de/prskr/nurse/config"
|
||||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||||
|
"code.icb4dc0.de/prskr/nurse/internal/retry"
|
||||||
"code.icb4dc0.de/prskr/nurse/validation"
|
"code.icb4dc0.de/prskr/nurse/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,31 +40,35 @@ func (g *GenericCheck) SetClient(client *http.Client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GenericCheck) Execute(ctx check.Context) error {
|
func (g *GenericCheck) Execute(ctx check.Context) error {
|
||||||
slog.Default().Debug("Execute check",
|
logger := slog.Default().With(
|
||||||
slog.String("check", "http"),
|
slog.String("check", "http"),
|
||||||
slog.String("method", g.Method),
|
slog.String("method", g.Method),
|
||||||
slog.String("url", g.URL),
|
slog.String("url", g.URL),
|
||||||
)
|
)
|
||||||
|
|
||||||
var body io.Reader
|
return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error {
|
||||||
if len(g.Body) > 0 {
|
logger.Debug("Execute check", slog.Int("attempt", attempt))
|
||||||
body = bytes.NewReader(g.Body)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, g.Method, g.URL, body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := g.Do(req)
|
var body io.Reader
|
||||||
if err != nil {
|
if len(g.Body) > 0 {
|
||||||
return err
|
body = bytes.NewReader(g.Body)
|
||||||
}
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, g.Method, g.URL, body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
resp, err := g.Do(req)
|
||||||
_ = resp.Body.Close()
|
if err != nil {
|
||||||
}()
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return g.validators.Validate(resp)
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return g.validators.Validate(resp)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GenericCheck) UnmarshalCheck(c grammar.Check, _ config.ServerLookup) error {
|
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 {
|
check.WithCheck("get", check.FactoryFunc(func() check.SystemChecker {
|
||||||
return new(GetCheck)
|
return new(GetCheck)
|
||||||
})),
|
})),
|
||||||
|
check.WithCheck("set", check.FactoryFunc(func() check.SystemChecker {
|
||||||
|
return new(SetCheck)
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
@ -56,6 +56,11 @@ func TestChecks_Execute(t *testing.T) {
|
||||||
check: `redis.PING("%s", "Hello, Redis!")`,
|
check: `redis.PING("%s", "Hello, Redis!")`,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "SET check",
|
||||||
|
check: `redis.SET("%s", "Hello", "World!")`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/nurse/internal/retry"
|
||||||
|
|
||||||
"code.icb4dc0.de/prskr/nurse/check"
|
"code.icb4dc0.de/prskr/nurse/check"
|
||||||
"code.icb4dc0.de/prskr/nurse/config"
|
"code.icb4dc0.de/prskr/nurse/config"
|
||||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||||
|
@ -21,34 +23,22 @@ type GetCheck struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GetCheck) Execute(ctx check.Context) error {
|
func (g *GetCheck) Execute(ctx check.Context) error {
|
||||||
slog.Default().Debug("Execute check",
|
logger := slog.Default().With(
|
||||||
slog.String("check", "redis.GET"),
|
slog.String("check", "redis.GET"),
|
||||||
slog.String("key", g.Key),
|
slog.String("key", g.Key),
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error {
|
||||||
select {
|
logger.Debug("Execute check", slog.Int("attempt", attempt))
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
cmd := g.Get(ctx, g.Key)
|
||||||
default:
|
|
||||||
attemptCtx, cancel := ctx.AttemptContext()
|
if err := cmd.Err(); err != nil {
|
||||||
err := g.executeAttempt(attemptCtx)
|
return err
|
||||||
cancel()
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GetCheck) executeAttempt(ctx context.Context) error {
|
return g.validators.Validate(cmd)
|
||||||
cmd := g.Get(ctx, g.Key)
|
})
|
||||||
|
|
||||||
if err := cmd.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return g.validators.Validate(cmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GetCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) error {
|
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/check"
|
||||||
"code.icb4dc0.de/prskr/nurse/config"
|
"code.icb4dc0.de/prskr/nurse/config"
|
||||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||||
|
"code.icb4dc0.de/prskr/nurse/internal/retry"
|
||||||
"code.icb4dc0.de/prskr/nurse/validation"
|
"code.icb4dc0.de/prskr/nurse/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,39 +22,27 @@ type PingCheck struct {
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PingCheck) Execute(ctx check.Context) error {
|
func (p *PingCheck) Execute(ctx check.Context) error {
|
||||||
slog.Default().Debug("Execute check",
|
logger := slog.Default().With(
|
||||||
slog.String("check", "redis.PING"),
|
slog.String("check", "redis.PING"),
|
||||||
slog.String("msg", p.Message),
|
slog.String("msg", p.Message),
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.Message == "" {
|
return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error {
|
||||||
return p.Ping(ctx).Err()
|
logger.Debug("Execute check", slog.Int("attempt", attempt))
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
if p.Message == "" {
|
||||||
select {
|
return p.Ping(ctx).Err()
|
||||||
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 {
|
||||||
if resp, err := p.Do(ctx, "PING", p.Message).Text(); err != nil {
|
return err
|
||||||
return err
|
} else if resp != p.Message {
|
||||||
} else if resp != p.Message {
|
return fmt.Errorf("expected value %s got %s", p.Message, resp)
|
||||||
return fmt.Errorf("expected value %s got %s", p.Message, resp)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PingCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) error {
|
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
|
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()
|
res, err := in.Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return g.comparator.Equals(res)
|
return g.comparator.Equals(res)
|
||||||
|
case *redis.StatusCmd:
|
||||||
|
if err := in.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"code.icb4dc0.de/prskr/nurse/check"
|
"code.icb4dc0.de/prskr/nurse/check"
|
||||||
"code.icb4dc0.de/prskr/nurse/config"
|
"code.icb4dc0.de/prskr/nurse/config"
|
||||||
"code.icb4dc0.de/prskr/nurse/grammar"
|
"code.icb4dc0.de/prskr/nurse/grammar"
|
||||||
|
"code.icb4dc0.de/prskr/nurse/internal/retry"
|
||||||
"code.icb4dc0.de/prskr/nurse/validation"
|
"code.icb4dc0.de/prskr/nurse/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,36 +45,25 @@ func (s *SelectCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SelectCheck) Execute(ctx check.Context) error {
|
func (s *SelectCheck) Execute(ctx check.Context) error {
|
||||||
slog.Default().Debug("Execute check",
|
logger := slog.Default().With(
|
||||||
slog.String("check", "sql.SELECT"),
|
slog.String("check", "sql.SELECT"),
|
||||||
slog.String("query", s.Query),
|
slog.String("query", s.Query),
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error {
|
||||||
select {
|
logger.Debug("Execute check", slog.Int("attempt", attempt))
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
logger.Debug("")
|
||||||
default:
|
|
||||||
attemptCtx, cancel := ctx.AttemptContext()
|
rows, err := s.QueryContext(ctx, s.Query)
|
||||||
err := s.executeAttempt(attemptCtx)
|
if err != nil {
|
||||||
cancel()
|
return err
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
defer func() {
|
||||||
|
err = errors.Join(rows.Close(), rows.Err())
|
||||||
func (s *SelectCheck) executeAttempt(ctx context.Context) (err error) {
|
}()
|
||||||
var rows *sql.Rows
|
|
||||||
rows, err = s.QueryContext(ctx, s.Query)
|
return s.validators.Validate(rows)
|
||||||
if err != nil {
|
})
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err = errors.Join(rows.Close(), rows.Err())
|
|
||||||
}()
|
|
||||||
|
|
||||||
return s.validators.Validate(rows)
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue