Skip to content

feat(healthcheck): Provided module #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
module:
- "config"
- "generate"
- "healthcheck"
- "log"
- "trace"

Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/healthcheck-ci.yml
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"
66 changes: 66 additions & 0 deletions healthcheck/.golangci.yml
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
140 changes: 140 additions & 0 deletions healthcheck/README.md
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
}
}
```
115 changes: 115 additions & 0 deletions healthcheck/checker.go
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,
}
}
Loading