This repository has been archived by the owner on Nov 13, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
app.go
114 lines (95 loc) · 2.61 KB
/
app.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// Package app implements OTEL, prometheus, graceful shutdown and other common application features
// for pfm projects.
package app
import (
"context"
"os"
"os/signal"
"time"
"github.com/go-faster/errors"
"github.com/go-faster/sdk/zctx"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/sync/errgroup"
)
const (
exitCodeOk = 0
exitCodeApplicationErr = 1
exitCodeWatchdog = 1
)
const (
shutdownTimeout = time.Second * 5
watchdogTimeout = shutdownTimeout + time.Second*5
)
// Run f until interrupt.
//
// If errors.Is(err, ctx.Err()) is valid for returned error, shutdown is considered graceful.
// Context is canceled on SIGINT. After watchdogTimeout application is forcefully terminated
// with exitCodeWatchdog.
func Run(f func(ctx context.Context, lg *zap.Logger, m *Metrics) error) {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
cfg := zap.NewProductionConfig()
if s := os.Getenv("OTEL_LOG_LEVEL"); s != "" {
var lvl zapcore.Level
if err := lvl.UnmarshalText([]byte(s)); err != nil {
panic(err)
}
cfg.Level.SetLevel(lvl)
}
lg, err := cfg.Build()
if err != nil {
panic(err)
}
// Add logger to root context.
ctx = zctx.Base(ctx, lg)
m, err := newMetrics(ctx, lg.Named("metrics"))
if err != nil {
panic(err)
}
g, ctx := errgroup.WithContext(ctx)
g.Go(func() (rerr error) {
defer func() {
if r := recover(); r != nil {
rerr = errors.Errorf("panic: %v", r)
}
}()
defer lg.Info("Shutting down")
if err := f(ctx, lg, m); err != nil {
if errors.Is(err, ctx.Err()) {
// Parent context got canceled, error is expected.
lg.Debug("Graceful shutdown")
return nil
}
return err
}
// Also shutting down metrics server to stop error group.
cancel()
return nil
})
g.Go(func() error {
if err := m.run(ctx); err != nil {
// Should already handle context cancellation gracefully.
return errors.Wrap(err, "metrics")
}
return nil
})
go func() {
// Guaranteed way to kill application.
// Helps if f is stuck, e.g. deadlock during shutdown.
<-ctx.Done()
// Context is canceled, giving application time to shut down gracefully.
lg.Info("Waiting for application shutdown")
time.Sleep(watchdogTimeout)
// Application is not shutting down gracefully, kill it.
// This code should not be executed if f is already returned.
lg.Warn("Graceful shutdown watchdog triggered: forcing shutdown")
os.Exit(exitCodeWatchdog)
}()
if err := g.Wait(); err != nil {
lg.Error("Failed", zap.Error(err))
os.Exit(exitCodeApplicationErr)
}
lg.Info("Application stopped")
os.Exit(exitCodeOk)
}