-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(healthcheck): Provided module (#9)
- Loading branch information
Showing
19 changed files
with
860 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ jobs: | |
module: | ||
- "config" | ||
- "generate" | ||
- "healthcheck" | ||
- "log" | ||
- "trace" | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
name: "healthcheck-ci" | ||
|
||
on: | ||
push: | ||
branches: | ||
- "feat**" | ||
- "fix**" | ||
- "hotfix**" | ||
- "chore**" | ||
paths: | ||
- "healthcheck/**.go" | ||
- "healthcheck/go.mod" | ||
- "healthcheck/go.sum" | ||
pull_request: | ||
types: | ||
- opened | ||
- synchronize | ||
- reopened | ||
branches: | ||
- main | ||
paths: | ||
- "healthcheck/**.go" | ||
- "healthcheck/go.mod" | ||
- "healthcheck/go.sum" | ||
|
||
jobs: | ||
ci: | ||
uses: ./.github/workflows/common-ci.yml | ||
secrets: inherit | ||
with: | ||
module: "healthcheck" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
# Health Check Module | ||
|
||
[![ci](https://github.com/ankorstore/yokai/actions/workflows/healthcheck-ci.yml/badge.svg)](https://github.com/ankorstore/yokai/actions/workflows/healthcheck-ci.yml) | ||
[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai/healthcheck)](https://goreportcard.com/report/github.com/ankorstore/yokai/healthcheck) | ||
[![codecov](https://codecov.io/gh/ankorstore/yokai/graph/badge.svg?token=5s0g5WyseS&flag=healthcheck)](https://app.codecov.io/gh/ankorstore/yokai/tree/main/healthcheck) | ||
[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai/healthcheck)](https://pkg.go.dev/github.com/ankorstore/yokai/healthcheck) | ||
|
||
> Health check module compatible with [K8s probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/). | ||
<!-- TOC --> | ||
|
||
* [Installation](#installation) | ||
* [Documentation](#documentation) | ||
* [Probes](#probes) | ||
* [Checker](#checker) | ||
|
||
<!-- TOC --> | ||
|
||
## Installation | ||
|
||
```shell | ||
go get github.com/ankorstore/yokai/healthcheck | ||
``` | ||
|
||
## Documentation | ||
|
||
This module provides a [Checker](checker.go), that: | ||
|
||
- can register any [CheckerProbe](probe.go) implementations and organise them for `startup`, `liveness` and / | ||
or `readiness` checks | ||
- and execute them to get an overall [CheckerResult](checker.go) | ||
|
||
The checker result will be considered as success if **ALL** registered probes checks are successful. | ||
|
||
### Probes | ||
|
||
This module provides a `CheckerProbe` interface to implement to provide your own probes, for example: | ||
|
||
```go | ||
package probes | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/ankorstore/yokai/healthcheck" | ||
) | ||
|
||
// 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") | ||
} | ||
|
||
// failure probe | ||
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") | ||
} | ||
``` | ||
|
||
Notes: | ||
|
||
- to perform more complex checks, you can inject dependencies to your probes implementation (ex: database, cache, etc) | ||
- it is recommended to design your probes with a single responsibility (ex: one for database, one for cache, etc) | ||
|
||
### Checker | ||
|
||
You can create a [Checker](checker.go) instance, register your [CheckerProbe](probe.go) implementations, and launch | ||
checks: | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"path/to/probes" | ||
"github.com/ankorstore/yokai/healthcheck" | ||
) | ||
|
||
func main() { | ||
ctx := context.Background() | ||
|
||
checker, _ := healthcheck.NewDefaultCheckerFactory().Create( | ||
healthcheck.WithProbe(probes.NewSuccessProbe()), // registers for startup, readiness and liveness | ||
healthcheck.WithProbe(probes.NewFailureProbe(), healthcheck.Liveness), // registers for liveness only | ||
) | ||
|
||
// startup health check: invoke only successProbe | ||
startupResult := checker.Check(ctx, healthcheck.Startup) | ||
|
||
fmt.Printf("startup check success: %v", startupResult.Success) // startup check success: true | ||
|
||
for probeName, probeResult := range startupResult.ProbesResults { | ||
fmt.Printf("probe name: %s, probe success: %v, probe message: %s", probeName, probeResult.Success, probeResult.Message) | ||
// probe name: successProbe, probe success: true, probe message: some success | ||
} | ||
|
||
// liveness health check: invoke successProbe and failureProbe | ||
livenessResult := checker.Check(ctx, healthcheck.Liveness) | ||
|
||
fmt.Printf("liveness check success: %v", livenessResult.Success) // liveness check success: false | ||
|
||
for probeName, probeResult := range livenessResult.ProbesResults { | ||
fmt.Printf("probe name: %s, probe success: %v, probe message: %s", probeName, probeResult.Success, probeResult.Message) | ||
// probe name: successProbe, probe success: true, probe message: some success | ||
// probe name: failureProbe, probe success: false, probe message: some failure | ||
} | ||
|
||
// readiness health check: invoke successProbe and failureProbe | ||
readinessResult := checker.Check(ctx, healthcheck.Readiness) | ||
|
||
fmt.Printf("readiness check success: %v", readinessResult.Success) // readiness check success: false | ||
|
||
for probeName, probeResult := range readinessResult.ProbesResults { | ||
fmt.Printf("probe name: %s, probe success: %v, probe message: %s", probeName, probeResult.Success, probeResult.Message) | ||
// probe name: successProbe, probe success: true, probe message: some success | ||
// probe name: failureProbe, probe success: false, probe message: some failure | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package healthcheck | ||
|
||
import "context" | ||
|
||
// CheckerResult is the result of a [Checker] check. | ||
// It contains a global status, and a list of [CheckerProbeResult] corresponding to each probe execution. | ||
type CheckerResult struct { | ||
Success bool `json:"success"` | ||
ProbesResults map[string]*CheckerProbeResult `json:"probes"` | ||
} | ||
|
||
// CheckerProbeRegistration represents a registration of a [CheckerProbe] in the [Checker]. | ||
type CheckerProbeRegistration struct { | ||
probe CheckerProbe | ||
kinds []ProbeKind | ||
} | ||
|
||
// NewCheckerProbeRegistration returns a [CheckerProbeRegistration], and accepts a [CheckerProbe] and an optional list of [ProbeKind]. | ||
// If no [ProbeKind] is provided, the [CheckerProbe] will be registered to be executed on all kinds of checks. | ||
func NewCheckerProbeRegistration(probe CheckerProbe, kinds ...ProbeKind) *CheckerProbeRegistration { | ||
return &CheckerProbeRegistration{ | ||
probe: probe, | ||
kinds: kinds, | ||
} | ||
} | ||
|
||
// Probe returns the [CheckerProbe] of the [CheckerProbeRegistration]. | ||
func (r *CheckerProbeRegistration) Probe() CheckerProbe { | ||
return r.probe | ||
} | ||
|
||
// Kinds returns the list of [ProbeKind] of the [CheckerProbeRegistration]. | ||
func (r *CheckerProbeRegistration) Kinds() []ProbeKind { | ||
return r.kinds | ||
} | ||
|
||
// Match returns true if the [CheckerProbeRegistration] match any of the provided [ProbeKind] list. | ||
func (r *CheckerProbeRegistration) Match(kinds ...ProbeKind) bool { | ||
for _, kind := range kinds { | ||
for _, registrationKind := range r.kinds { | ||
if registrationKind == kind { | ||
return true | ||
} | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
// Checker provides the possibility to register several [CheckerProbe] and execute them. | ||
type Checker struct { | ||
registrations map[string]*CheckerProbeRegistration | ||
} | ||
|
||
// NewChecker returns a [Checker] instance. | ||
func NewChecker() *Checker { | ||
return &Checker{ | ||
registrations: map[string]*CheckerProbeRegistration{}, | ||
} | ||
} | ||
|
||
// Probes returns the list of [CheckerProbe] registered for the provided list of [ProbeKind]. | ||
// If no [ProbeKind] is provided, probes matching all kinds will be returned. | ||
func (c *Checker) Probes(kinds ...ProbeKind) []CheckerProbe { | ||
var probes []CheckerProbe | ||
|
||
if len(kinds) == 0 { | ||
kinds = []ProbeKind{Startup, Liveness, Readiness} | ||
} | ||
|
||
for _, registration := range c.registrations { | ||
if registration.Match(kinds...) { | ||
probes = append(probes, registration.probe) | ||
} | ||
} | ||
|
||
return probes | ||
} | ||
|
||
// RegisterProbe registers a [CheckerProbe] for an optional list of [ProbeKind]. | ||
// If no [ProbeKind] is provided, the [CheckerProbe] will be registered for all kinds. | ||
func (c *Checker) RegisterProbe(probe CheckerProbe, kinds ...ProbeKind) *Checker { | ||
if len(kinds) == 0 { | ||
kinds = []ProbeKind{Startup, Liveness, Readiness} | ||
} | ||
|
||
if _, ok := c.registrations[probe.Name()]; ok { | ||
c.registrations[probe.Name()].kinds = kinds | ||
} else { | ||
c.registrations[probe.Name()] = NewCheckerProbeRegistration(probe, kinds...) | ||
} | ||
|
||
return c | ||
} | ||
|
||
// Check executes all the registered probes for a [ProbeKind], passes a [context.Context] to each of them, and returns a [CheckerResult]. | ||
// The [CheckerResult] is successful if all probes executed with success. | ||
func (c *Checker) Check(ctx context.Context, kind ProbeKind) *CheckerResult { | ||
probeResults := map[string]*CheckerProbeResult{} | ||
|
||
success := true | ||
for name, registration := range c.registrations { | ||
if registration.Match(kind) { | ||
pr := registration.probe.Check(ctx) | ||
|
||
success = success && pr.Success | ||
probeResults[name] = pr | ||
} | ||
} | ||
|
||
return &CheckerResult{ | ||
Success: success, | ||
ProbesResults: probeResults, | ||
} | ||
} |
Oops, something went wrong.