Skip to content

Commit

Permalink
feat: init project
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotxx committed Jul 21, 2023
0 parents commit 4e234f5
Show file tree
Hide file tree
Showing 23 changed files with 1,614 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
33 changes: 33 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Upload Go test results

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:

runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.18.x', '1.19.x', '1.20.x' ]

steps:
- uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Install dependencies
run: go get .
- name: Test with Go
run: go test -race -json > TestResults-${{ matrix.go-version }}.json
- name: Test
uses: actions/upload-artifact@v3
with:
name: Go-results-${{ matrix.go-version }}
path: TestResults-${{ matrix.go-version }}.json
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 ElliotXX

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
222 changes: 222 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# Healthcheck
[![Go Reference](https://pkg.go.dev/badge/github.com/elliotxx/healthcheck.svg)](https://pkg.go.dev/github.com/elliotxx/healthcheck)
![tests](https://github.com/elliotxx/healthcheck/actions/workflows/test.yaml/badge.svg)

This module will create two **kubernetes-style** endpoints (`/livez` and `/readyz`) for Gin framework,
which can be used to determine the healthiness of Gin application.

**Modify based on [tavsec/gin-healthcheck](https://github.com/tavsec/gin-healthcheck).**


## Installation
Install package:
```shell
go get github.com/elliotxx/healthcheck
```

## Usage
```go
package main

import (
"github.com/gin-gonic/gin"
healthcheck "github.com/elliotxx/healthcheck"
"github.com/elliotxx/healthcheck/checks"
"github.com/elliotxx/healthcheck/config"
)

func main() {
r := gin.Default()

healthcheck.New(r, config.DefaultConfig(), []checks.Check{})

r.Run()
}
```

This will add the healthcheck endpoint to the default path, which is `/healthz`. The path can be customized
using `config.Config` structure. In the example above, no specific checks will be included, only API availability.

## Health checks

### SQL
Currently, healthcheck comes with SQL check, which will send `ping` request to SQL.

```go
package main

import (
"database/sql"
"github.com/gin-gonic/gin"
healthcheck "github.com/elliotxx/healthcheck"
"github.com/elliotxx/healthcheck/checks"
"github.com/elliotxx/healthcheck/config"
)

func main() {
r := gin.Default()

db, _ := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
sqlCheck := checks.SqlCheck{Sql: db}
healthcheck.New(r, config.DefaultConfig(), []checks.Check{sqlCheck})

r.Run()
}
```

### Ping
In case you want to ensure that your application can reach a separate service, you can utilize `PingCheck`.

```go
package main

import (
"github.com/gin-gonic/gin"
healthcheck "github.com/elliotxx/healthcheck"
"github.com/elliotxx/healthcheck/checks"
"github.com/elliotxx/healthcheck/config"
)

func main() {
r := gin.Default()

pingCheck := checks.NewPingCheck("https://www.google.com", "GET", 1000, nil, nil)
healthcheck.New(r, config.DefaultConfig(), []checks.Check{pingCheck})

r.Run()
```
### Redis check
You can perform Redis ping check using `RedisCheck` checker:
```go
package main

import (
"github.com/gin-gonic/gin"
healthcheck "github.com/elliotxx/healthcheck"
"github.com/elliotxx/healthcheck/checks"
"github.com/elliotxx/healthcheck/config"
"github.com/redis/go-redis/v9"
)

func main() {
r := gin.Default()

rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
redisCheck := checks.NewRedisCheck(rdb)
healthcheck.New(r, config.DefaultConfig(), []checks.Check{redisCheck})

r.Run()
```
### Environmental variables check
You can check if an environmental variable is set using `EnvCheck`:
```go
package main

import (
"github.com/gin-gonic/gin"
healthcheck "github.com/elliotxx/healthcheck"
"github.com/elliotxx/healthcheck/checks"
"github.com/elliotxx/healthcheck/config"
)

func main(){
r := gin.Default()

dbHostCheck := checks.NewEnvCheck("DB_HOST")

// You can also validate env format using regex
dbUserCheck := checks.NewEnvCheck("DB_HOST")
dbUserCheck.SetRegexValidator("^USER_")

healthcheck.New(r, config.DefaultConfig(), []checks.Check{dbHostCheck, dbUserCheck})

r.Run()
}
```
### context.Context check
You can check if a context has not been canceled, by using a `ContextCheck`:
```go
package main

import (
"context"
"net/http"
"os/signal"
"syscall"

"github.com/gin-gonic/gin"
healthcheck "github.com/elliotxx/healthcheck"
"github.com/elliotxx/healthcheck/checks"
"github.com/elliotxx/healthcheck/config"
)

func main(){
r := gin.Default()

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

signalsCheck := checks.NewContextCheck(ctx, "signals")
healthcheck.New(r, config.DefaultConfig(), []checks.Check{signalsCheck})

r.Run()
}
```
### Custom checks
Besides built-in health checks, you can extend the functionality and create your own check, utilizing the `Check` interface:
```go
package checks

type Check interface {
Pass() bool
Name() string
}
```
## Notification of health check failure
It is possible to get notified when the health check failed a certain threshold of call. This would match for example the failureThreshold of Kubernetes and allow us to take action in that case.
```go
package main

import (
"github.com/gin-gonic/gin"
healthcheck "github.com/elliotxx/healthcheck"
"github.com/elliotxx/healthcheck/checks"
)

func main() {
r := gin.Default()

conf := healthcheck.DefaultConfig()

conf.FailureNotification.Chan = make(chan error, 1)
defer close(conf.FailureNotification.Chan)
conf.FailureNotification.Threshold = 3

go func() {
<-conf.FailureNotification.Chan
os.Exit(1)
}

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

signalsCheck := checks.NewContextCheck(ctx, "signals")
healthcheck.New(r, conf, []checks.Check{signalsCheck})

r.Run()
}
```
Note that the following example is not doing a graceful shutdown. If Kubernetes is set up with a failureThreshold of 3, it will mark the pod as failing after that third call, but there is no guarantee that you have processed and answered all HTTP requests before the call to os.Exit(1). It is necessary to use something like https://github.com/gin-contrib/graceful at that point to have a graceful shutdown.
6 changes: 6 additions & 0 deletions checks/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package checks

type Check interface {
Pass() bool
Name() string
}
54 changes: 54 additions & 0 deletions checks/context_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package checks

import (
"context"
"runtime"
"sync/atomic"
)

type contextCheck struct {
name string
terminated uint32 // TODO: When the minimal supported base go version is 1.19, use atomic.Bool
ctx context.Context
}

func NewContextCheck(ctx context.Context, name ...string) Check {
if len(name) > 1 {
panic("context check does only accept one name")
}
if ctx == nil {
panic("context check needs a context")
}

contextName := "Unknown"
if len(name) == 1 {
contextName = name[0]
} else {
pc, _, _, ok := runtime.Caller(1)
details := runtime.FuncForPC(pc)
if ok && details != nil {
contextName = details.Name()
}
}

c := contextCheck{
name: contextName,
ctx: ctx,
}

go func() {
<-ctx.Done()
atomic.StoreUint32(&c.terminated, 1)
}()

return &c
}

func (c *contextCheck) Pass() bool {
v := atomic.LoadUint32(&c.terminated)
return v == 0
}

func (c *contextCheck) Name() string {
return c.name
}
Loading

0 comments on commit 4e234f5

Please sign in to comment.