-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathutils.go
271 lines (243 loc) · 7.13 KB
/
utils.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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
package common
import (
"context"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"math/big"
"net"
"net/http"
"os"
"reflect"
"runtime"
"strings"
"github.com/fsnotify/fsnotify"
"github.com/getsentry/sentry-go"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
const (
randomLength = 32
sentryKey = "sentry"
)
func init() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
log.Logger = zerolog.New(os.Stderr)
log.Logger = log.With().Logger()
}
var characterRunes = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// RandomString returns a random string length of argument n.
func RandomString(n int) (string, error) {
b := make([]byte, n)
for i := range b {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(characterRunes))))
if err != nil {
return "", err
}
b[i] = characterRunes[num.Int64()]
}
return string(b), nil
}
// RandomToken returns random sha256 string.
func RandomToken() (string, error) {
hash := sha256.New()
r, err := RandomString(randomLength)
if err != nil {
return "", err
}
hash.Write([]byte(r))
bs := hash.Sum(nil)
return fmt.Sprintf("%x", bs), nil
}
// IsHTTPS is a helper function that evaluates the http.Request
// and returns True if the Request uses HTTPS. It is able to detect,
// using the X-Forwarded-Proto, if the original request was HTTPS and
// routed through a reverse proxy with SSL termination.
func IsHTTPS(r *http.Request) bool {
switch {
case r.URL.Scheme == "https":
return true
case r.TLS != nil:
return true
case strings.HasPrefix(r.Proto, "HTTPS"):
return true
case r.Header.Get("X-Forwarded-Proto") == "https":
return true
default:
return false
}
}
// MinUint calculates Min from a, b.
func MinUint(a, b uint) uint {
if a < b {
return a
}
return b
}
// EnsureDot ensures that string has ending dot.
func EnsureDot(input string) string {
if !strings.HasSuffix(input, ".") {
return fmt.Sprintf("%s.", input)
}
return input
}
// RemoveDot removes suffix dot from string if it exists.
func RemoveDot(input string) string {
if strings.HasSuffix(input, ".") {
return input[:len(input)-1]
}
return input
}
// LoadAndListenConfig loads config file to struct and listen changes in it.
func LoadAndListenConfig(path string, obj interface{}, onUpdate func(oldObj interface{})) error {
v := viper.New()
v.SetConfigFile(path)
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("unable to read config: %w", err)
}
if err := v.Unmarshal(&obj); err != nil {
return fmt.Errorf("unable to marshal config: %w", err)
}
log.Info().
Str("path", v.ConfigFileUsed()).
Msg("config loaded")
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
log.Info().
Str("path", e.Name).
Msg("config reloaded")
oldObj := reflect.Indirect(reflect.ValueOf(obj)).Interface()
if err := v.Unmarshal(&obj); err != nil {
log.Fatal().
Str("path", e.Name).
Msgf("unable to marshal config: %v", err)
}
if onUpdate != nil {
onUpdate(oldObj)
}
})
return nil
}
// Recovery middleware for Sentry crash reporting.
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := c.Request.Context()
_, transaction, hub := MakeSentryTransaction(
ctx,
fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path),
sentry.ContinueFromRequest(c.Request),
sentry.WithTransactionSource(sentry.SourceURL),
)
defer transaction.Finish()
c.Request = c.Request.WithContext(transaction.Context())
hub.Scope().SetRequest(c.Request)
c.Set(sentryKey, hub)
defer recoverWithSentry(hub, c.Request)
c.Next()
}
}
// RecoverWithContext recovers from panic and sends it to Sentry.
func RecoverWithContext(ctx context.Context, transaction *sentry.Span) {
if transaction != nil {
transaction.Finish()
}
if err := recover(); err != nil {
defer sentry.RecoverWithContext(ctx)
panic(err)
}
}
// Check for a broken connection, as this is what Gin does already.
func isBrokenPipeError(err interface{}) bool {
if netErr, ok := err.(*net.OpError); ok {
var se *os.SyscallError
if errors.As(netErr, &se) {
seStr := strings.ToLower(se.Error())
if strings.Contains(seStr, "broken pipe") ||
strings.Contains(seStr, "connection reset by peer") {
return true
}
}
}
return false
}
func recoverWithSentry(hub *sentry.Hub, r *http.Request) {
if err := recover(); err != nil {
if !isBrokenPipeError(err) {
_ = hub.RecoverWithContext(
context.WithValue(r.Context(), sentry.RequestContextKey, r),
err,
)
}
panic(err)
}
}
// MakeSentryTransaction creates Sentry transaction.
func MakeSentryTransaction(ctx context.Context, name string, opts ...sentry.SpanOption) (context.Context, *sentry.Span, *sentry.Hub) {
var hub *sentry.Hub
ctx, hub = setHubToContext(ctx)
options := []sentry.SpanOption{
sentry.WithOpName(name),
}
options = append(options, opts...)
transaction := sentry.StartTransaction(ctx,
name,
options...,
)
return transaction.Context(), transaction, hub
}
func setHubToContext(ctx context.Context) (context.Context, *sentry.Hub) {
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub().Clone()
ctx = sentry.SetHubOnContext(ctx, hub)
}
return ctx, hub
}
// sentrySpanTracer middleware for sentry span time reporting.
func sentrySpanTracer() gin.HandlerFunc {
return func(c *gin.Context) {
span := sentry.StartSpan(c.Request.Context(), c.HandlerName())
defer span.Finish()
c.Next()
}
}
// MakeSpan makes new sentry span.
func MakeSpan(ctx context.Context, skip int) *sentry.Span {
pc, _, _, _ := runtime.Caller(skip) //nolint:dogsled
tmp := runtime.FuncForPC(pc)
spanName := "nil"
if tmp != nil {
spanName = tmp.Name()
}
span := sentry.StartSpan(ctx, spanName)
return span
}
// GET wrapper to include sentrySpanTracer as last middleware.
func GET(group *gin.RouterGroup, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return group.Handle(http.MethodGet, relativePath, addSpanTracer(handlers)...)
}
// PUT wrapper to include sentrySpanTracer as last middleware.
func PUT(group *gin.RouterGroup, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return group.Handle(http.MethodPut, relativePath, addSpanTracer(handlers)...)
}
// POST wrapper to include sentrySpanTracer as last middleware.
func POST(group *gin.RouterGroup, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return group.Handle(http.MethodPost, relativePath, addSpanTracer(handlers)...)
}
// DELETE wrapper to include sentrySpanTracer as last middleware.
func DELETE(group *gin.RouterGroup, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return group.Handle(http.MethodDelete, relativePath, addSpanTracer(handlers)...)
}
// PATCH wrapper to include sentrySpanTracer as last middleware.
func PATCH(group *gin.RouterGroup, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return group.Handle(http.MethodPatch, relativePath, addSpanTracer(handlers)...)
}
func addSpanTracer(handlers []gin.HandlerFunc) []gin.HandlerFunc {
lastElement := handlers[len(handlers)-1]
handlers = handlers[:len(handlers)-1]
handlers = append(handlers, sentrySpanTracer(), lastElement)
return handlers
}