-
Notifications
You must be signed in to change notification settings - Fork 9
/
event.go
209 lines (180 loc) · 5.73 KB
/
event.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
package slog
import (
"context"
"fmt"
"time"
uuid "github.com/nu7hatch/gouuid"
)
type Severity int
const (
ErrorMetadataKey = "error"
TimeFormat = "2006-01-02 15:04:05-0700 (MST)"
TraceSeverity Severity = 1
DebugSeverity Severity = 2
InfoSeverity Severity = 3
WarnSeverity Severity = 4
ErrorSeverity Severity = 5
CriticalSeverity Severity = 6
)
func (s Severity) String() string {
switch s {
case CriticalSeverity:
return "CRITICAL"
case ErrorSeverity:
return "ERROR"
case WarnSeverity:
return "WARN"
case InfoSeverity:
return "INFO"
case DebugSeverity:
return "DEBUG"
default:
return "TRACE"
}
}
type logMetadataProvider interface {
LogMetadata() map[string]string
}
// An Event is a discrete logging event
type Event struct {
Context context.Context `json:"-"`
Id string `json:"id"`
Timestamp time.Time `json:"timestamp"`
Severity Severity `json:"severity"`
Message string `json:"message"`
OriginalMessage string `json:"-"`
// Metadata are structured key-value pairs which describe the event.
Metadata map[string]interface{} `json:"meta,omitempty"`
// Labels, like Metadata, are key-value pairs which describe the event. Unlike Metadata, these are intended to be
// indexed.
Labels map[string]string `json:"labels,omitempty"`
Error interface{} `json:"error,omitempty"`
}
func (e Event) String() string {
errorMessage := ""
if e.Error != nil {
if err, ok := e.Error.(error); ok {
errorMessage = err.Error()
}
}
return fmt.Sprintf("[%s] %s %s (error=%v metadata=%v labels=%v id=%s)", e.Timestamp.Format(TimeFormat),
e.Severity.String(), e.Message, errorMessage, e.Metadata, e.Labels, e.Id)
}
// Eventf constructs an event from the given message string and formatting operands. Optionally, event metadata
// (map[string]interface{}, or map[string]string) can be provided as a final argument.
func Eventf(sev Severity, ctx context.Context, msg string, params ...interface{}) Event {
originalMessage := msg
if ctx == nil {
ctx = context.Background()
}
id, err := uuid.NewV4()
if err != nil {
return Event{}
}
metadata := map[string]interface{}(nil)
var errParam error
if len(params) > 0 {
fmtOperands := countFmtOperands(msg)
// If we have been provided with more params than we have formatting arguments, then we have
// been given some metadata.
extraParamCount := len(params) - fmtOperands
// If we've got more fmtOperands than params, we have an invalid log statement.
// In this case, we do our best to extract metadata from existing params, and
// we also write as many as we can into the string.
// For example, if you give: log("foo %s %s", err), we'll end up with "foo {err} %!s(MISSING)"
// _and_ metadata extracted from the error.
//
// We do this so that we have the highest chance of actually capturing important details.
// The alternative is erroring loudly, but as we see this in the wild for cases which are
// rarely exercised and probably not covered in tests (e.g. error paths), I don't think
// there's a better alternative.
hasFormatOverflow := false
if extraParamCount < 0 {
hasFormatOverflow = true
extraParamCount = len(params)
}
// Attempt to pull metadata and errors from any params.
// This means that we'll still extract errors and metadata, even if it
// is going to be interpolated into the message. This may result in some
// duplication, but always gives us the most structured data possible.
if len(params) > 0 {
metadata = mergeMetadata(metadata, metadataFromParams(params))
errParam = extractFirstErrorParam(params)
}
// If any of the provided params can be "upgraded" to a logMetadataProvider i.e.
// they themselves have a LogMetadata method that returns a map[string]string
// then we merge these params with the metadata.
for _, param := range params {
param, ok := param.(logMetadataProvider)
if !ok {
continue
}
metadata = mergeMetadata(metadata, stringMapToInterfaceMap(param.LogMetadata()))
}
if fmtOperands > 0 {
endIndex := len(params) - extraParamCount
if hasFormatOverflow {
endIndex = len(params)
}
nonMetaParams := params[0:endIndex]
msg = fmt.Sprintf(msg, nonMetaParams...)
}
}
event := Event{
Context: ctx,
Id: id.String(),
Timestamp: time.Now().UTC(),
Severity: sev,
Message: msg,
OriginalMessage: originalMessage,
Metadata: metadata,
Error: errParam,
}
return event
}
func extractFirstErrorParam(params []interface{}) error {
for _, param := range params {
err, ok := param.(error)
if !ok {
continue
}
return err
}
return nil
}
func metadataFromParams(params []interface{}) map[string]interface{} {
result := map[string]interface{}(nil)
for _, param := range params {
// This is deprecated, but continue to support a map of strings.
if metadataParam, ok := param.(map[string]string); ok {
result = mergeMetadata(result, stringMapToInterfaceMap(metadataParam))
}
// Check for 'raw' metadata rather than strings.
if metadataParam, ok := param.(map[string]interface{}); ok {
result = mergeMetadata(result, metadataParam)
}
}
return result
}
func stringMapToInterfaceMap(m map[string]string) map[string]interface{} {
shim := make(map[string]interface{}, len(m))
for k, v := range m {
shim[k] = v
}
return shim
}
// mergeMetadata merges the metadata but preserves existing entries
func mergeMetadata(current, new map[string]interface{}) map[string]interface{} {
if len(new) == 0 {
return current
}
if current == nil {
current = map[string]interface{}{}
}
for k, v := range new {
if _, ok := current[k]; !ok {
current[k] = v
}
}
return current
}