Skip to content

Commit

Permalink
feat(healthcheck): Provided module (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
ekkinox committed Jan 10, 2024
1 parent 677e841 commit 03131b5
Show file tree
Hide file tree
Showing 19 changed files with 860 additions and 0 deletions.
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

0 comments on commit 03131b5

Please sign in to comment.