diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6a52c1b5..83cc3d00 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -31,6 +31,7 @@ jobs: - "trace" - "fxconfig" - "fxgenerate" + - "fxhealthcheck" - "fxlog" - "fxmetrics" - "fxtrace" diff --git a/.github/workflows/fxhealthcheck-ci.yml b/.github/workflows/fxhealthcheck-ci.yml new file mode 100644 index 00000000..6b66f8d7 --- /dev/null +++ b/.github/workflows/fxhealthcheck-ci.yml @@ -0,0 +1,31 @@ +name: "fxhealthcheck-ci" + +on: + push: + branches: + - "feat**" + - "fix**" + - "hotfix**" + - "chore**" + paths: + - "fxhealthcheck/**.go" + - "fxhealthcheck/go.mod" + - "fxhealthcheck/go.sum" + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - main + paths: + - "fxhealthcheck/**.go" + - "fxhealthcheck/go.mod" + - "fxhealthcheck/go.sum" + +jobs: + ci: + uses: ./.github/workflows/common-ci.yml + secrets: inherit + with: + module: "fxhealthcheck" diff --git a/README.md b/README.md index 7cf78c50..c9a24487 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,14 @@ Yokai is using [Fx](https://github.com/uber-go/fx) for its plugin system. Yokai's `Fx modules` are the plugins for your Yokai application. -| Fx Module | Description | -|--------------------------|-------------------------------------------------------------------------| -| [fxconfig](fxconfig) | Fx module for [config](config) | -| [fxgenerate](fxgenerate) | Fx module for [generate](generate) | -| [fxlog](fxlog) | Fx module for [log](log) | -| [fxmetrics](fxmetrics) | Fx module for [prometheus](https://github.com/prometheus/client_golang) | -| [fxtrace](fxtrace) | Fx module for [trace](trace) | +| Fx Module | Description | +|--------------------------------|-------------------------------------------------------------------------| +| [fxconfig](fxconfig) | Fx module for [config](config) | +| [fxgenerate](fxgenerate) | Fx module for [generate](generate) | +| [fxhealthcheck](fxhealthcheck) | Fx module for [healthcheck](healthcheck) | +| [fxlog](fxlog) | Fx module for [log](log) | +| [fxmetrics](fxmetrics) | Fx module for [prometheus](https://github.com/prometheus/client_golang) | +| [fxtrace](fxtrace) | Fx module for [trace](trace) | They can also be used in any [Fx](https://github.com/uber-go/fx) based Go application. diff --git a/fxhealthcheck/.golangci.yml b/fxhealthcheck/.golangci.yml new file mode 100644 index 00000000..edf0e9ec --- /dev/null +++ b/fxhealthcheck/.golangci.yml @@ -0,0 +1,66 @@ +run: + timeout: 5m + concurrency: 8 + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - cyclop + - decorder + - dogsled + - dupl + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - exhaustive + - forbidigo + - forcetypeassert + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - gofmt + - goheader + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - govet + - grouper + - importas + - ineffassign + - interfacebloat + - logrlint + - maintidx + - makezero + - misspell + - nestif + - nilerr + - nilnil + - nlreturn + - nolintlint + - nosprintfhostport + - prealloc + - predeclared + - promlinter + - reassign + - staticcheck + - tenv + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - whitespace diff --git a/fxhealthcheck/README.md b/fxhealthcheck/README.md new file mode 100644 index 00000000..740f3f59 --- /dev/null +++ b/fxhealthcheck/README.md @@ -0,0 +1,165 @@ +# Fx Health Check Module + +[![ci](https://github.com/ankorstore/yokai/actions/workflows/fxhealthcheck-ci.yml/badge.svg)](https://github.com/ankorstore/yokai/actions/workflows/fxhealthcheck-ci.yml) +[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai/fxhealthcheck)](https://goreportcard.com/report/github.com/ankorstore/yokai/fxhealthcheck) +[![codecov](https://codecov.io/gh/ankorstore/yokai/graph/badge.svg?token=ghUBlFsjhR&flag=fxhealthcheck)](https://app.codecov.io/gh/ankorstore/yokai/tree/main/fxhealthcheck) +[![Deps](https://img.shields.io/badge/osi-deps-blue)](https://deps.dev/go/github.com%2Fankorstore%2Fyokai%2Ffxhealthcheck) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai/fxhealthcheck)](https://pkg.go.dev/github.com/ankorstore/yokai/fxhealthcheck) + +> [Fx](https://uber-go.github.io/fx/) module for [healthcheck](https://github.com/ankorstore/yokai/tree/main/healthcheck). + + + +* [Installation](#installation) +* [Documentation](#documentation) + * [Loading](#loading) + * [Registration](#registration) + * [Override](#override) + + + +## Installation + +```shell +go get github.com/ankorstore/yokai/fxhealthcheck +``` + +## Documentation + +### Loading + +To load the module in your Fx application: + +```go +package main + +import ( + "context" + "fmt" + + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/healthcheck" + "go.uber.org/fx" +) + +func main() { + fx.New( + fxhealthcheck.FxHealthcheckModule, // load the module + fx.Invoke(func(checker *healthcheck.Checker) { // invoke the checker for liveness checks + fmt.Printf("checker result: %v", checker.Check(context.Background(), healthcheck.Liveness)) + }), + ).Run() +} +``` + +### Registration + +This module provides the possibility to register +several [CheckerProbe](https://github.com/ankorstore/yokai/blob/main/healthcheck/probe.go) implementations, and organise +them for `startup`, `liveness` and / +or `readiness` [checks](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/). + +They will be then collected and given by Fx to +the [Checker](https://github.com/ankorstore/yokai/blob/main/healthcheck/checker.go), made available in the Fx container. + +This is done via the `AsCheckerProbe()` function: + +```go +package main + +import ( + "context" + "fmt" + + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/healthcheck" + "go.uber.org/fx" +) + +// example success probe +type SuccessProbe struct{} + +func NewSuccessProbe() *SuccessProbe { + return &SuccessProbe{} +} + +func (p *SuccessProbe) Name() string { + return "successProbe" +} + +func (p *SuccessProbe) Check(ctx context.Context) *healthcheck.CheckerProbeResult { + return healthcheck.NewCheckerProbeResult(true, "some success") +} + +// example failure probe +type FailureProbe struct{} + +func NewFailureProbe() *FailureProbe { + return &FailureProbe{} +} + +func (p *FailureProbe) Name() string { + return "someProbe" +} + +func (p *FailureProbe) Check(ctx context.Context) *healthcheck.CheckerProbeResult { + return healthcheck.NewCheckerProbeResult(false, "some failure") +} + +// usage +func main() { + fx.New( + fxhealthcheck.FxHealthcheckModule, // load the module + fx.Provide( + fxhealthcheck.AsCheckerProbe(NewSuccessProbe), // register the SuccessProbe probe for startup, liveness and readiness checks + fxhealthcheck.AsCheckerProbe(NewFailureProbe, healthcheck.Liveness), // register the FailureProbe probe for liveness checks only + ), + fx.Invoke(func(checker *healthcheck.Checker) { // invoke the checker + ctx := context.Background() + + fmt.Printf("startup: %v", checker.Check(ctx, healthcheck.Startup).Success) // startup: true + fmt.Printf("liveness: %v", checker.Check(ctx, healthcheck.Liveness).Success) // liveness: false + fmt.Printf("readiness: %v", checker.Check(ctx, healthcheck.Readiness).Success) // readiness: true + }), + ).Run() +} +``` + +### Override + +By default, the `healthcheck.Checker` is created by +the [DefaultCheckerFactory](https://github.com/ankorstore/yokai/blob/main/healthcheck/factory.go). + +If needed, you can provide your own factory and override the module: + +```go +package main + +import ( + "context" + + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/healthcheck" + "go.uber.org/fx" +) + +type CustomCheckerFactory struct{} + +func NewCustomCheckerFactory() healthcheck.CheckerFactory { + return &CustomCheckerFactory{} +} + +func (f *CustomCheckerFactory) Create(options ...healthcheck.CheckerOption) (*healthcheck.Checker, error) { + return &healthcheck.Checker{...}, nil +} + +func main() { + fx.New( + fxhealthcheck.FxHealthcheckModule, // load the module + fx.Decorate(NewCustomCheckerFactory), // override the module with a custom factory + fx.Invoke(func(checker *healthcheck.Checker) { // invoke the custom checker for readiness checks + checker.Check(context.Background(), healthcheck.Readiness) + }), + ).Run() +} +``` diff --git a/fxhealthcheck/define.go b/fxhealthcheck/define.go new file mode 100644 index 00000000..8da303c2 --- /dev/null +++ b/fxhealthcheck/define.go @@ -0,0 +1,32 @@ +package fxhealthcheck + +import "github.com/ankorstore/yokai/healthcheck" + +// CheckerProbeDefinition is the interface for probes definitions. +type CheckerProbeDefinition interface { + ReturnType() string + Kinds() []healthcheck.ProbeKind +} + +type checkerProbeDefinition struct { + returnType string + kinds []healthcheck.ProbeKind +} + +// NewCheckerProbeDefinition returns a new [CheckerProbeDefinition]. +func NewCheckerProbeDefinition(returnType string, kinds ...healthcheck.ProbeKind) CheckerProbeDefinition { + return &checkerProbeDefinition{ + returnType: returnType, + kinds: kinds, + } +} + +// ReturnType returns the probe return type. +func (c *checkerProbeDefinition) ReturnType() string { + return c.returnType +} + +// Kinds returns the probe registration kinds. +func (c *checkerProbeDefinition) Kinds() []healthcheck.ProbeKind { + return c.kinds +} diff --git a/fxhealthcheck/define_test.go b/fxhealthcheck/define_test.go new file mode 100644 index 00000000..da849267 --- /dev/null +++ b/fxhealthcheck/define_test.go @@ -0,0 +1,19 @@ +package fxhealthcheck_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/healthcheck" + "github.com/stretchr/testify/assert" +) + +func TestNewCheckerProbeDefinition(t *testing.T) { + t.Parallel() + + definition := fxhealthcheck.NewCheckerProbeDefinition("test", healthcheck.Liveness, healthcheck.Readiness) + + assert.Implements(t, (*fxhealthcheck.CheckerProbeDefinition)(nil), definition) + assert.Equal(t, "test", definition.ReturnType()) + assert.Equal(t, []healthcheck.ProbeKind{healthcheck.Liveness, healthcheck.Readiness}, definition.Kinds()) +} diff --git a/fxhealthcheck/go.mod b/fxhealthcheck/go.mod new file mode 100644 index 00000000..607f7803 --- /dev/null +++ b/fxhealthcheck/go.mod @@ -0,0 +1,20 @@ +module github.com/ankorstore/yokai/fxhealthcheck + +go 1.20 + +require ( + github.com/ankorstore/yokai/healthcheck v1.0.0 + github.com/stretchr/testify v1.8.4 + go.uber.org/fx v1.20.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/dig v1.17.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.23.0 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/fxhealthcheck/go.sum b/fxhealthcheck/go.sum new file mode 100644 index 00000000..64951f94 --- /dev/null +++ b/fxhealthcheck/go.sum @@ -0,0 +1,31 @@ +github.com/ankorstore/yokai/healthcheck v1.0.0 h1:uX6RrchsvbxCV70dh5d6RX5LEuGIf+Pt+14waV0CzY0= +github.com/ankorstore/yokai/healthcheck v1.0.0/go.mod h1:Frz73NuG8ruLDz04vQxzf0bWhKK1Ru2Ktod+3ltaIxs= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= +go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= +go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= +go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/fxhealthcheck/module.go b/fxhealthcheck/module.go new file mode 100644 index 00000000..3ca3ee52 --- /dev/null +++ b/fxhealthcheck/module.go @@ -0,0 +1,43 @@ +package fxhealthcheck + +import ( + "github.com/ankorstore/yokai/healthcheck" + "go.uber.org/fx" +) + +// ModuleName is the module name. +const ModuleName = "healthcheck" + +// FxHealthcheckModule is the [Fx] healthcheck module. +// +// [Fx]: https://github.com/uber-go/fx +var FxHealthcheckModule = fx.Module( + ModuleName, + fx.Provide( + healthcheck.NewDefaultCheckerFactory, + NewFxCheckerProbeRegistry, + NewFxChecker, + ), +) + +// FxCheckerParam allows injection of the required dependencies in [NewFxChecker]. +type FxCheckerParam struct { + fx.In + Factory healthcheck.CheckerFactory + Registry *CheckerProbeRegistry +} + +// NewFxChecker returns a new [healthcheck.Checker]. +func NewFxChecker(p FxCheckerParam) (*healthcheck.Checker, error) { + registrations, err := p.Registry.ResolveCheckerProbesRegistrations() + if err != nil { + return nil, err + } + + options := []healthcheck.CheckerOption{} + for _, registration := range registrations { + options = append(options, healthcheck.WithProbe(registration.Probe(), registration.Kinds()...)) + } + + return p.Factory.Create(options...) +} diff --git a/fxhealthcheck/module_test.go b/fxhealthcheck/module_test.go new file mode 100644 index 00000000..f7361258 --- /dev/null +++ b/fxhealthcheck/module_test.go @@ -0,0 +1,88 @@ +package fxhealthcheck_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/fxhealthcheck/testdata/factory" + "github.com/ankorstore/yokai/fxhealthcheck/testdata/probes" + "github.com/ankorstore/yokai/healthcheck" + "github.com/stretchr/testify/assert" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" +) + +func TestModule(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + var checker *healthcheck.Checker + + fxtest.New( + t, + fx.NopLogger, + fxhealthcheck.FxHealthcheckModule, + fx.Options( + fxhealthcheck.AsCheckerProbe(probes.NewSuccessProbe), + fxhealthcheck.AsCheckerProbe(probes.NewFailureProbe, healthcheck.Liveness, healthcheck.Readiness), + ), + fx.Populate(&checker), + ).RequireStart().RequireStop() + + // startup probes checks + result := checker.Check(ctx, healthcheck.Startup) + assert.True(t, result.Success) + + data, err := json.Marshal(result) + assert.Nil(t, err) + assert.Equal(t, + `{"success":true,"probes":{"successProbe":{"success":true,"message":"some success"}}}`, + string(data), + ) + + // liveness probes checks + result = checker.Check(ctx, healthcheck.Liveness) + assert.False(t, result.Success) + + data, err = json.Marshal(result) + assert.Nil(t, err) + assert.Equal(t, + `{"success":false,"probes":{"failureProbe":{"success":false,"message":"some failure"},"successProbe":{"success":true,"message":"some success"}}}`, + string(data), + ) + + // readiness probes checks + result = checker.Check(ctx, healthcheck.Readiness) + assert.False(t, result.Success) + + data, err = json.Marshal(result) + assert.Nil(t, err) + assert.Equal(t, + `{"success":false,"probes":{"failureProbe":{"success":false,"message":"some failure"},"successProbe":{"success":true,"message":"some success"}}}`, + string(data), + ) +} + +func TestModuleDecoration(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + var checker *healthcheck.Checker + + fxtest.New( + t, + fx.NopLogger, + fxhealthcheck.FxHealthcheckModule, + fx.Decorate(factory.NewTestCheckerFactory), + fx.Populate(&checker), + ).RequireStart().RequireStop() + + // NewTestCheckerFactory registers failureProbe only for readiness + assert.True(t, checker.Check(ctx, healthcheck.Startup).Success) + assert.True(t, checker.Check(ctx, healthcheck.Liveness).Success) + assert.False(t, checker.Check(ctx, healthcheck.Readiness).Success) +} diff --git a/fxhealthcheck/reflect.go b/fxhealthcheck/reflect.go new file mode 100644 index 00000000..d6d18802 --- /dev/null +++ b/fxhealthcheck/reflect.go @@ -0,0 +1,15 @@ +package fxhealthcheck + +import ( + "reflect" +) + +// GetType returns the type of a target. +func GetType(target any) string { + return reflect.TypeOf(target).String() +} + +// GetReturnType returns the return type of a target. +func GetReturnType(target any) string { + return reflect.TypeOf(target).Out(0).String() +} diff --git a/fxhealthcheck/reflect_test.go b/fxhealthcheck/reflect_test.go new file mode 100644 index 00000000..3c2ca639 --- /dev/null +++ b/fxhealthcheck/reflect_test.go @@ -0,0 +1,56 @@ +package fxhealthcheck_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/fxhealthcheck/testdata/probes" + "github.com/stretchr/testify/assert" +) + +func TestGetType(t *testing.T) { + t.Parallel() + + tests := []struct { + target any + expected string + }{ + {123, "int"}, + {"test", "string"}, + {probes.NewSuccessProbe(), "*probes.SuccessProbe"}, + {probes.NewFailureProbe(), "*probes.FailureProbe"}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.expected, func(t *testing.T) { + t.Parallel() + + got := fxhealthcheck.GetType(tt.target) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestGetReturnType(t *testing.T) { + t.Parallel() + + tests := []struct { + target any + expected string + }{ + {func() string { return "test" }, "string"}, + {func() int { return 123 }, "int"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.expected, func(t *testing.T) { + t.Parallel() + + got := fxhealthcheck.GetReturnType(tt.target) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/fxhealthcheck/register.go b/fxhealthcheck/register.go new file mode 100644 index 00000000..0e8e04c1 --- /dev/null +++ b/fxhealthcheck/register.go @@ -0,0 +1,26 @@ +package fxhealthcheck + +import ( + "github.com/ankorstore/yokai/healthcheck" + "go.uber.org/fx" +) + +// AsCheckerProbe registers a [healthcheck.CheckerProbe] into Fx. +func AsCheckerProbe(p any, kinds ...healthcheck.ProbeKind) fx.Option { + return fx.Options( + fx.Provide( + fx.Annotate( + p, + fx.As(new(healthcheck.CheckerProbe)), + fx.ResultTags(`group:"healthcheck-probes"`), + ), + ), + fx.Supply( + fx.Annotate( + NewCheckerProbeDefinition(GetReturnType(p), kinds...), + fx.As(new(CheckerProbeDefinition)), + fx.ResultTags(`group:"healthcheck-probes-definitions"`), + ), + ), + ) +} diff --git a/fxhealthcheck/register_test.go b/fxhealthcheck/register_test.go new file mode 100644 index 00000000..71d056f7 --- /dev/null +++ b/fxhealthcheck/register_test.go @@ -0,0 +1,19 @@ +package fxhealthcheck_test + +import ( + "fmt" + "testing" + + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/fxhealthcheck/testdata/probes" + "github.com/ankorstore/yokai/healthcheck" + "github.com/stretchr/testify/assert" +) + +func TestAsCheckerProbe(t *testing.T) { + t.Parallel() + + result := fxhealthcheck.AsCheckerProbe(probes.NewSuccessProbe, healthcheck.Startup) + + assert.Equal(t, "fx.optionGroup", fmt.Sprintf("%T", result)) +} diff --git a/fxhealthcheck/registry.go b/fxhealthcheck/registry.go new file mode 100644 index 00000000..928ae821 --- /dev/null +++ b/fxhealthcheck/registry.go @@ -0,0 +1,58 @@ +package fxhealthcheck + +import ( + "fmt" + + "github.com/ankorstore/yokai/healthcheck" + "go.uber.org/fx" +) + +// CheckerProbeRegistry is the registry collecting probes and their definitions. +type CheckerProbeRegistry struct { + probes []healthcheck.CheckerProbe + definitions []CheckerProbeDefinition +} + +// FxCheckerProbeRegistryParam allows injection of the required dependencies in [NewFxCheckerProbeRegistry]. +type FxCheckerProbeRegistryParam struct { + fx.In + Probes []healthcheck.CheckerProbe `group:"healthcheck-probes"` + Definitions []CheckerProbeDefinition `group:"healthcheck-probes-definitions"` +} + +// NewFxCheckerProbeRegistry returns as new [CheckerProbeRegistry]. +func NewFxCheckerProbeRegistry(p FxCheckerProbeRegistryParam) *CheckerProbeRegistry { + return &CheckerProbeRegistry{ + probes: p.Probes, + definitions: p.Definitions, + } +} + +// ResolveCheckerProbesRegistrations resolves [healthcheck.CheckerProbeRegistration] from their definitions. +func (r *CheckerProbeRegistry) ResolveCheckerProbesRegistrations() ([]*healthcheck.CheckerProbeRegistration, error) { + registrations := []*healthcheck.CheckerProbeRegistration{} + + for _, definition := range r.definitions { + implementation, err := r.lookupRegisteredCheckerProbe(definition.ReturnType()) + if err != nil { + return nil, err + } + + registrations = append( + registrations, + healthcheck.NewCheckerProbeRegistration(implementation, definition.Kinds()...), + ) + } + + return registrations, nil +} + +func (r *CheckerProbeRegistry) lookupRegisteredCheckerProbe(returnType string) (healthcheck.CheckerProbe, error) { + for _, implementation := range r.probes { + if GetType(implementation) == returnType { + return implementation, nil + } + } + + return nil, fmt.Errorf("cannot find checker probe implementation for type %s", returnType) +} diff --git a/fxhealthcheck/registry_test.go b/fxhealthcheck/registry_test.go new file mode 100644 index 00000000..1a5b8a8d --- /dev/null +++ b/fxhealthcheck/registry_test.go @@ -0,0 +1,67 @@ +package fxhealthcheck_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/fxhealthcheck/testdata/probes" + "github.com/ankorstore/yokai/healthcheck" + "github.com/stretchr/testify/assert" +) + +func TestNewCheckerProbeRegistry(t *testing.T) { + t.Parallel() + + param := fxhealthcheck.FxCheckerProbeRegistryParam{ + Probes: []healthcheck.CheckerProbe{}, + Definitions: []fxhealthcheck.CheckerProbeDefinition{}, + } + registry := fxhealthcheck.NewFxCheckerProbeRegistry(param) + + assert.IsType(t, &fxhealthcheck.CheckerProbeRegistry{}, registry) +} + +func TestResolveCheckerProbesRegistrationsSuccess(t *testing.T) { + t.Parallel() + + param := fxhealthcheck.FxCheckerProbeRegistryParam{ + Probes: []healthcheck.CheckerProbe{ + probes.NewSuccessProbe(), + probes.NewFailureProbe(), + }, + Definitions: []fxhealthcheck.CheckerProbeDefinition{ + fxhealthcheck.NewCheckerProbeDefinition("*probes.SuccessProbe", healthcheck.Liveness), + fxhealthcheck.NewCheckerProbeDefinition("*probes.FailureProbe", healthcheck.Readiness), + }, + } + + registry := fxhealthcheck.NewFxCheckerProbeRegistry(param) + + registrations, err := registry.ResolveCheckerProbesRegistrations() + assert.NoError(t, err) + + assert.Len(t, registrations, 2) + assert.IsType(t, &probes.SuccessProbe{}, registrations[0].Probe()) + assert.Equal(t, []healthcheck.ProbeKind{healthcheck.Liveness}, registrations[0].Kinds()) + assert.IsType(t, &probes.FailureProbe{}, registrations[1].Probe()) + assert.Equal(t, []healthcheck.ProbeKind{healthcheck.Readiness}, registrations[1].Kinds()) +} + +func TestResolveCheckerProbesRegistrationsFailure(t *testing.T) { + t.Parallel() + + param := fxhealthcheck.FxCheckerProbeRegistryParam{ + Probes: []healthcheck.CheckerProbe{ + probes.NewSuccessProbe(), + }, + Definitions: []fxhealthcheck.CheckerProbeDefinition{ + fxhealthcheck.NewCheckerProbeDefinition("invalid", healthcheck.Liveness), + }, + } + + registry := fxhealthcheck.NewFxCheckerProbeRegistry(param) + + _, err := registry.ResolveCheckerProbesRegistrations() + assert.Error(t, err) + assert.Equal(t, "cannot find checker probe implementation for type invalid", err.Error()) +} diff --git a/fxhealthcheck/testdata/factory/factory.go b/fxhealthcheck/testdata/factory/factory.go new file mode 100644 index 00000000..3587ded1 --- /dev/null +++ b/fxhealthcheck/testdata/factory/factory.go @@ -0,0 +1,19 @@ +package factory + +import ( + "github.com/ankorstore/yokai/fxhealthcheck/testdata/probes" + "github.com/ankorstore/yokai/healthcheck" +) + +type TestCheckerFactory struct{} + +func NewTestCheckerFactory() healthcheck.CheckerFactory { + return &TestCheckerFactory{} +} + +func (f *TestCheckerFactory) Create(options ...healthcheck.CheckerOption) (*healthcheck.Checker, error) { + checker := healthcheck.NewChecker() + checker.RegisterProbe(probes.NewFailureProbe(), healthcheck.Readiness) + + return checker, nil +} diff --git a/fxhealthcheck/testdata/probes/failure.go b/fxhealthcheck/testdata/probes/failure.go new file mode 100644 index 00000000..ea5fd685 --- /dev/null +++ b/fxhealthcheck/testdata/probes/failure.go @@ -0,0 +1,21 @@ +package probes + +import ( + "context" + + "github.com/ankorstore/yokai/healthcheck" +) + +type FailureProbe struct{} + +func NewFailureProbe() *FailureProbe { + return &FailureProbe{} +} + +func (p *FailureProbe) Name() string { + return "failureProbe" +} + +func (p *FailureProbe) Check(ctx context.Context) *healthcheck.CheckerProbeResult { + return healthcheck.NewCheckerProbeResult(false, "some failure") +} diff --git a/fxhealthcheck/testdata/probes/success.go b/fxhealthcheck/testdata/probes/success.go new file mode 100644 index 00000000..b41b5fe6 --- /dev/null +++ b/fxhealthcheck/testdata/probes/success.go @@ -0,0 +1,21 @@ +package probes + +import ( + "context" + + "github.com/ankorstore/yokai/healthcheck" +) + +type SuccessProbe struct{} + +func NewSuccessProbe() *SuccessProbe { + return &SuccessProbe{} +} + +func (p *SuccessProbe) Name() string { + return "successProbe" +} + +func (p *SuccessProbe) Check(ctx context.Context) *healthcheck.CheckerProbeResult { + return healthcheck.NewCheckerProbeResult(true, "some success") +} diff --git a/release-please-config.json b/release-please-config.json index a3761f75..837f1a6a 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -51,6 +51,11 @@ "component": "fxgenerate", "tag-separator": "/" }, + "fxhealthcheck": { + "release-type": "go", + "component": "fxhealthcheck", + "tag-separator": "/" + }, "fxlog": { "release-type": "go", "component": "fxlog",