-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 4e234f5
Showing
23 changed files
with
1,614 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 |
---|---|---|
@@ -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" |
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,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 |
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 @@ | ||
.idea |
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,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. |
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,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. |
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,6 @@ | ||
package checks | ||
|
||
type Check interface { | ||
Pass() bool | ||
Name() string | ||
} |
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,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 | ||
} |
Oops, something went wrong.