/
core.go
258 lines (235 loc) · 8.23 KB
/
core.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
// Copyright 2023 SpotHero
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sentry
import (
"fmt"
"math"
"regexp"
"time"
"github.com/getsentry/sentry-go"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// duration to wait to flush events to sentry
const flushTimeout = 2 * time.Second
// Core Implements a zapcore.Core that sends logged errors to Sentry
type Core struct {
zapcore.LevelEnabler
withFields []zapcore.Field
}
// With adds structured context to the Sentry Core
func (c *Core) With(fields []zapcore.Field) zapcore.Core {
if len(fields) == 0 {
return c
}
clonedLogger := *c
clonedLogger.withFields = append(clonedLogger.withFields, fields...)
return &clonedLogger
}
// Check must be called before calling Write. This determines whether logs are sent to
// Sentry
func (c *Core) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
// send error logs and above to Sentry
if ent.Level >= zapcore.ErrorLevel {
return ce.AddCore(ent, c)
}
return ce
}
// filter out function calls from this module and from the logger in stack traces
// reported to sentry
var stacktraceModulesToIgnore = []*regexp.Regexp{
regexp.MustCompile(`github\.com/spothero/tools/sentry*`),
regexp.MustCompile(`github\.com/uber-go/zap*`),
regexp.MustCompile(`go\.uber\.org/zap*`),
}
// Write logs the entry and fields supplied at the log site and writes them to their destination. If a
// Sentry Hub field is present in the fields, that Hub will be used for reporting to Sentry, otherwise
// the default Sentry Hub will be used.
func (c *Core) Write(ent zapcore.Entry, fields []zapcore.Field) error {
var severity sentry.Level
switch ent.Level {
case zapcore.DebugLevel:
severity = sentry.LevelDebug
case zapcore.InfoLevel:
severity = sentry.LevelInfo
case zapcore.WarnLevel:
severity = sentry.LevelWarning
case zapcore.ErrorLevel:
severity = sentry.LevelError
default:
// captures Panic, DPanic, Fatal zapcore levels
severity = sentry.LevelFatal
}
// Add extra logged fields to the Sentry packet
// This block was adapted from the way zap encodes messages internally
// See https://github.com/uber-go/zap/blob/v1.7.1/zapcore/field.go#L107
sentryExtra := make(map[string]interface{})
fingerprints := make([]string, 0)
tags := make(map[string]string)
mergedFields := fields
if len(c.withFields) > 0 {
mergedFields = append(mergedFields, c.withFields...)
}
hub := sentry.CurrentHub()
for _, field := range mergedFields {
switch field.Type {
case zapcore.ArrayMarshalerType:
sentryExtra[field.Key] = field.Interface
case zapcore.ObjectMarshalerType:
sentryExtra[field.Key] = field.Interface
case zapcore.BinaryType:
sentryExtra[field.Key] = field.Interface
case zapcore.BoolType:
sentryExtra[field.Key] = field.Integer == 1
case zapcore.ByteStringType:
sentryExtra[field.Key] = field.Interface
case zapcore.Complex128Type:
sentryExtra[field.Key] = field.Interface
case zapcore.Complex64Type:
sentryExtra[field.Key] = field.Interface
case zapcore.DurationType:
sentryExtra[field.Key] = field.Integer
case zapcore.Float64Type:
sentryExtra[field.Key] = math.Float64frombits(uint64(field.Integer))
case zapcore.Float32Type:
sentryExtra[field.Key] = math.Float32frombits(uint32(field.Integer))
case zapcore.Int64Type:
sentryExtra[field.Key] = field.Integer
case zapcore.Int32Type:
sentryExtra[field.Key] = field.Integer
case zapcore.Int16Type:
sentryExtra[field.Key] = field.Integer
case zapcore.Int8Type:
sentryExtra[field.Key] = field.Integer
case zapcore.StringType:
sentryExtra[field.Key] = field.String
case zapcore.TimeType:
if field.Interface != nil {
// Time has a timezone
sentryExtra[field.Key] = time.Unix(0, field.Integer).In(field.Interface.(*time.Location))
} else {
sentryExtra[field.Key] = time.Unix(0, field.Integer)
}
case zapcore.Uint64Type:
sentryExtra[field.Key] = uint64(field.Integer)
case zapcore.Uint32Type:
sentryExtra[field.Key] = uint32(field.Integer)
case zapcore.Uint16Type:
sentryExtra[field.Key] = uint16(field.Integer)
case zapcore.Uint8Type:
sentryExtra[field.Key] = uint8(field.Integer)
case zapcore.UintptrType:
sentryExtra[field.Key] = uintptr(field.Integer)
case zapcore.ReflectType:
sentryExtra[field.Key] = field.Interface
case zapcore.NamespaceType:
sentryExtra[field.Key] = field.Interface
case zapcore.StringerType:
sentryExtra[field.Key] = field.Interface.(fmt.Stringer).String()
case zapcore.ErrorType:
sentryExtra[field.Key] = field.Interface.(error).Error()
case zapcore.SkipType:
if field.Key == loggerFieldKey {
if h, ok := field.Interface.(hubZapField); ok {
hub = h.Hub
}
}
if field.Interface == TagType {
tags[field.Key] = field.String
} else if field.Interface == FingerprintType {
fingerprints = append(fingerprints, field.String)
}
default:
sentryExtra[field.Key] = fmt.Sprintf("Unknown field type %v", field.Type)
}
}
// Group logs with the same message
fingerprint := ent.Message
event := sentry.NewEvent()
event.Message = ent.Message
event.Level = severity
event.Logger = ent.LoggerName
event.Timestamp = ent.Time
event.Extra = sentryExtra
event.Tags = tags
event.Fingerprint = append(fingerprints, fingerprint)
stackTrace := sentry.NewStacktrace()
filteredFrames := make([]sentry.Frame, 0, len(stackTrace.Frames))
for _, frame := range stackTrace.Frames {
ignoreFrame := false
for _, pattern := range stacktraceModulesToIgnore {
if pattern.MatchString(frame.Module) {
ignoreFrame = true
break
}
}
if !ignoreFrame {
filteredFrames = append(filteredFrames, frame)
}
}
event.Threads = []sentry.Thread{{
Stacktrace: &sentry.Stacktrace{
Frames: filteredFrames,
},
Current: true,
}}
hub.CaptureEvent(event)
// level higher than error, (i.e. panic, fatal), the program might crash,
// so block while sentry sends the event
if ent.Level > zapcore.ErrorLevel {
hub.Flush(flushTimeout)
}
return nil
}
// Sync flushes any buffered logs
func (c *Core) Sync() error {
if !sentry.Flush(flushTimeout) {
return fmt.Errorf("timed out waiting for Sentry flush")
}
return nil
}
const loggerFieldKey = "sentry"
type hubZapField struct {
*sentry.Hub
}
// Hub attaches a Sentry hub to the logger such that if the logger ever logs an
// error, request context can be sent to Sentry.
func Hub(hub *sentry.Hub) zapcore.Field {
// This is a hack in order to pass an arbitrary object (in this case a Sentry Hub) through the logger so
// that it can be pulled out in the custom Zap core. The way this works is the sentry Hub is wrapped in a type
// that implements Zap's ObjectMarshaler as a no-op. That object gets set on the zap.Field's Interface key
// The Type key is set to SkipType and the Key field is set to some unique value. This makes it so the built-in
// logger cores ignore the field, but in the custom Sentry core above we can check if the Key matches and try
// to pull the Sentry hub out of the field.
return zap.Field{
Key: loggerFieldKey,
Type: zapcore.SkipType,
Interface: hubZapField{hub},
}
}
// MarshalLogObject implements Zap's ObjectMarshaler interface but is a no-op
// since we don't actually want to add anything from the Sentry Hub to the log.
func (f hubZapField) MarshalLogObject(_ zapcore.ObjectEncoder) error {
return nil
}
const TagType = "sentry-tag"
// Tag attaches a tag which will be indexed by Sentry and searchable.
func Tag(key, value string) zap.Field {
return zap.Field{Key: key, Type: zapcore.SkipType, String: value, Interface: TagType}
}
const FingerprintType = "sentry-fingerprint"
// Fingerprint are used to group events into issues
func Fingerprint(key, value string) zap.Field {
return zap.Field{Key: key, Type: zapcore.SkipType, String: value, Interface: FingerprintType}
}