-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlog_to_bug.go
181 lines (149 loc) · 5.23 KB
/
log_to_bug.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
package slogbugsnag
import (
"context"
"fmt"
"log/slog"
"runtime"
"strings"
"time"
"github.com/bugsnag/bugsnag-go/v2"
)
// bugsnagUserID is a sentinel interface that gives you another option to
// customize how bugsnag fills the ID in the "User" tab
type bugsnagUserID interface {
BugsnagUserID() string
}
// bugsnagUserName is a sentinel interface that gives you another option to
// customize how bugsnag fills the Name in the "User" tab
type bugsnagUserName interface {
BugsnagUserName() string
}
// bugsnagUserEmail is a sentinel interface that gives you another option to
// customize how bugsnag fills the Email in the "User" tab
type bugsnagUserEmail interface {
BugsnagUserEmail() string
}
var _ bugsnagUserID = ID("") // Validate implements interface
// ID is a string that, if used as a log attribute value, will be filled in
// on the [bugsnag.User] struct. The ID is also used to determine the number
// of users affected by the bug, in bugsnag's system.
type ID string
// BugsnagUserID returns the bugsnag user id
func (id ID) BugsnagUserID() string {
return string(id)
}
var _ bugsnagUserName = Name("") // Validate implements interface
// Name is a string that, if used as a log attribute value, will be filled in
// on the [bugsnag.User] struct.
type Name string
// BugsnagUserName returns the bugsnag user name
func (name Name) BugsnagUserName() string {
return string(name)
}
var _ bugsnagUserEmail = Email("") // Validate implements interface
// Email is a string that, if used as a log attribute value, will be filled in
// on the [bugsnag.User] struct.
type Email string
// BugsnagUserEmail returns the bugsnag user email
func (email Email) BugsnagUserEmail() string {
return string(email)
}
// bug type contains everything needed to be sent off to bugsnag, preformatted
type bugRecord struct {
err error
rawData []any
}
// logToBug creates and formats a bug, from a log record and attributes.
// The level of the error should be checked if sufficient or not before calling.
func (h *Handler) logToBug(ctx context.Context, t time.Time, lvl slog.Level, msg string, pc uintptr, attrs []slog.Attr) bugRecord {
// Do we report this bugsnag as unhandled or handled?
var unhandled bool
if lvl >= h.unhandledLevel.Level() {
unhandled = true
}
// Format the log source line
frameStack := runtime.CallersFrames([]uintptr{pc})
frame, _ := frameStack.Next()
source := fmt.Sprintf("%s:%d", frame.Function, frame.Line)
// Find the errors and bugsnag.User's in the log attributes.
// Create MetaData for all the other information in the log.
var errForBugsnag error
user := bugsnag.User{}
md := bugsnag.MetaData{}
h.accumulateRawData(&errForBugsnag, &user, md, "log", attrs)
// Add in the log record info
md.Add("log", "time", t.Format(time.RFC3339Nano))
md.Add("log", "level", lvl.String())
md.Add("log", "msg", msg)
md.Add("log", "source", source)
// Ensure the error is not nil and has a stack trace
errForBugsnag = newErrorWithStack(errForBugsnag, msg, pc)
// The order matters
rawData := []any{
ctx,
bugsnag.Context{String: msg},
bugsnag.HandledState{Unhandled: unhandled},
bsSeverity(lvl), // Must come after HandledState
md,
}
if user.Id != "" || user.Name != "" || user.Email != "" {
rawData = append(rawData, user)
}
return bugRecord{err: errForBugsnag, rawData: rawData}
}
// accumulateRawData recursively iterates through all attributes and turns them
// into [bugsnag.MetaData] tabs. The log tab is used for all root-level attributes.
// All attributes in groups get their own tab, named after the group.
// Attribute values are redacted based on the notifier config ParamsFilters.
// accumulateRawData also finds the latest [error] and [bugsnag.User].
func (h *Handler) accumulateRawData(errForBugsnag *error, user *bugsnag.User, md bugsnag.MetaData, tab string, attrs []slog.Attr) {
for _, attr := range attrs {
if attr.Value.Kind() == slog.KindGroup {
h.accumulateRawData(errForBugsnag, user, md, attr.Key, attr.Value.Group())
continue
}
// Because the attributes slice we are iterating through is ordered from
// oldest to newest, we should overwrite the error/user to get the latest one.
// Because there could be multiple, we still add these to the MetaData map.
switch t := attr.Value.Any().(type) {
case error:
if t != nil {
*errForBugsnag = t
}
case bugsnag.User:
*user = t
case bugsnagUserID:
user.Id = t.BugsnagUserID()
case bugsnagUserName:
user.Name = t.BugsnagUserName()
case bugsnagUserEmail:
user.Email = t.BugsnagUserEmail()
}
// Replace with filtered if the key matches
if shouldRedact(attr.Key, h.notifiers.notifier.Config.ParamsFilters) {
md.Add(tab, attr.Key, "[FILTERED]")
continue
}
// Always resolve log attribute values
attr.Value = attr.Value.Resolve()
md.Add(tab, attr.Key, attr.Value.Any())
}
}
func shouldRedact(key string, filters []string) bool {
for _, filter := range filters {
if strings.Contains(strings.ToLower(key), strings.ToLower(filter)) {
return true
}
}
return false
}
// bsSeverity converts a [slog.Level] to a [bugsnag.severity]
func bsSeverity(lvl slog.Level) any {
if lvl < slog.LevelWarn {
return bugsnag.SeverityInfo
}
if lvl < slog.LevelError {
return bugsnag.SeverityWarning
}
return bugsnag.SeverityError
}