-
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
Showing
11 changed files
with
309 additions
and
3 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
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
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
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
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
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,20 @@ | ||
package db | ||
|
||
import ( | ||
"context" | ||
"database/sql" | ||
) | ||
|
||
type Probe struct { | ||
db *sql.DB | ||
} | ||
|
||
func NewProbe(db *sql.DB) Probe { | ||
return Probe{ | ||
db: db, | ||
} | ||
} | ||
|
||
func (p Probe) Check(ctx context.Context) error { | ||
return p.db.PingContext(ctx) | ||
} |
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,24 @@ | ||
package discord | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/bwmarrin/discordgo" | ||
) | ||
|
||
type Probe struct { | ||
discord *discordgo.Session | ||
} | ||
|
||
func NewProbe( | ||
discord *discordgo.Session, | ||
) Probe { | ||
return Probe{ | ||
discord: discord, | ||
} | ||
} | ||
|
||
func (p Probe) Check(_ context.Context) error { | ||
_, err := p.discord.User("@me") | ||
return err | ||
} |
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,151 @@ | ||
package healthcheck | ||
|
||
import ( | ||
"context" | ||
"log" | ||
"log/slog" | ||
"sync" | ||
"time" | ||
|
||
"github.com/valyala/fasthttp" | ||
) | ||
|
||
type Healthcheck struct { | ||
log *slog.Logger | ||
probes map[string]Probe | ||
timeoutDegraded time.Duration | ||
timeoutUnhealthy time.Duration | ||
} | ||
|
||
func New( | ||
log *slog.Logger, | ||
opts ...Option, | ||
) *Healthcheck { | ||
hc := &Healthcheck{ | ||
log: log, | ||
probes: map[string]Probe{}, | ||
timeoutDegraded: 1 * time.Second, | ||
timeoutUnhealthy: 10 * time.Second, | ||
} | ||
|
||
for _, opt := range opts { | ||
opt(hc) | ||
} | ||
|
||
return hc | ||
} | ||
|
||
func (hc *Healthcheck) Serve(address string) *fasthttp.Server { | ||
s := &fasthttp.Server{ | ||
Handler: func(ctx *fasthttp.RequestCtx) { | ||
if string(ctx.Path()) != "/health" || !ctx.IsGet() { | ||
ctx.NotFound() | ||
return | ||
} | ||
|
||
status := hc.handle(ctx) | ||
|
||
if status == StatusHealthy { | ||
ctx.SetStatusCode(fasthttp.StatusOK) | ||
} else { | ||
ctx.SetStatusCode(fasthttp.StatusServiceUnavailable) | ||
} | ||
ctx.SetBodyString(status.String()) | ||
}, | ||
} | ||
|
||
go func() { | ||
err := s.ListenAndServe(address) | ||
if err != nil { | ||
log.Fatalln("healthcheck server error", "error", err) | ||
} | ||
}() | ||
|
||
return s | ||
} | ||
|
||
func (hc *Healthcheck) handle(ctx context.Context) Status { | ||
nProbes := len(hc.probes) | ||
|
||
wg := &sync.WaitGroup{} | ||
wg.Add(nProbes) | ||
|
||
statuses := make(chan Status, nProbes) | ||
|
||
for name, probe := range hc.probes { | ||
hc.probeCheck( | ||
hc.log.With("probe", name), ctx, wg, probe, statuses, | ||
) | ||
} | ||
|
||
wg.Wait() | ||
close(statuses) | ||
|
||
status := StatusHealthy | ||
for s := range statuses { | ||
if s <= status { | ||
continue | ||
} | ||
|
||
status = s | ||
if status == StatusUnhealthy { | ||
break | ||
} | ||
} | ||
|
||
return status | ||
} | ||
|
||
func (hc *Healthcheck) probeCheck( | ||
log *slog.Logger, | ||
ctx context.Context, | ||
wg *sync.WaitGroup, | ||
probe Probe, | ||
status chan Status, | ||
) { | ||
timedCtx, cancel := context.WithTimeout(ctx, hc.timeoutUnhealthy) | ||
|
||
defer func() { | ||
if err := recover(); err != nil { | ||
log.ErrorContext( | ||
ctx, | ||
"probe panicked", | ||
"panic", err, | ||
) | ||
|
||
status <- StatusUnhealthy | ||
} | ||
|
||
cancel() | ||
wg.Done() | ||
}() | ||
|
||
probeTime := time.Now() | ||
err := probe.Check(timedCtx) | ||
probeDuration := time.Since(probeTime) | ||
|
||
if err != nil || probeDuration > hc.timeoutUnhealthy { | ||
log.ErrorContext( | ||
ctx, | ||
"failed to probe", | ||
"error", err, | ||
"duration", probeDuration.String(), | ||
) | ||
|
||
status <- StatusUnhealthy | ||
return | ||
} | ||
|
||
if probeDuration > hc.timeoutDegraded { | ||
log.WarnContext( | ||
ctx, | ||
"probe is degraded", | ||
"duration", probeDuration.String(), | ||
) | ||
|
||
status <- StatusDegraded | ||
return | ||
} | ||
|
||
status <- StatusHealthy | ||
} |
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,39 @@ | ||
package healthcheck | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
) | ||
|
||
type Option func(hc *Healthcheck) | ||
|
||
func WithProbe(name string, probe Probe) Option { | ||
return func(hc *Healthcheck) { | ||
if _, ok := hc.probes[name]; ok { | ||
p := fmt.Sprintf("healthcheck probe '%s' already registered", name) | ||
panic(p) | ||
} | ||
|
||
hc.probes[name] = probe | ||
} | ||
} | ||
|
||
func WithTimeoutDegraded(timeout time.Duration) Option { | ||
return func(hc *Healthcheck) { | ||
if timeout <= 0 { | ||
panic("healthcheck timeout must be greater than zero") | ||
} | ||
|
||
hc.timeoutDegraded = timeout | ||
} | ||
} | ||
|
||
func WithTimeoutUnhealthy(timeout time.Duration) Option { | ||
return func(hc *Healthcheck) { | ||
if timeout <= 0 { | ||
panic("healthcheck timeout must be greater than zero") | ||
} | ||
|
||
hc.timeoutUnhealthy = timeout | ||
} | ||
} |
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,9 @@ | ||
package healthcheck | ||
|
||
import ( | ||
"context" | ||
) | ||
|
||
type Probe interface { | ||
Check(ctx context.Context) error | ||
} |
Oops, something went wrong.