Skip to content

Commit 642c0fe

Browse files
authored
feature(formatters): add JSONFormatter (#57)
* feature(formatters): add JSONFormatter Signed-off-by: Kyle Squizzato <kyle@replicated.com>
1 parent d41fed6 commit 642c0fe

File tree

3 files changed

+174
-56
lines changed

3 files changed

+174
-56
lines changed

log/formatters.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package log
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"fmt"
67
"path/filepath"
78
"strings"
@@ -64,3 +65,61 @@ func shortPath(pathIn string) string {
6465

6566
return strings.Join(resultToks, string(filepath.Separator))
6667
}
68+
69+
type JSONFormatter struct{}
70+
71+
var DefaultJSONFieldKeys = []string{"level", "timestamp", "caller", "message"}
72+
73+
func (jf *JSONFormatter) Format(entry *logrus.Entry) ([]byte, error) {
74+
data := make(logrus.Fields, len(entry.Data)+4)
75+
for k, v := range entry.Data {
76+
if strings.HasPrefix(k, "saaskit.") {
77+
continue
78+
}
79+
80+
switch v := v.(type) {
81+
case error:
82+
// Otherwise errors are ignored by `encoding/json`
83+
// https://github.com/sirupsen/logrus/issues/137
84+
data[k] = v.Error()
85+
default:
86+
data[k] = v
87+
}
88+
}
89+
90+
// Configure default fields.
91+
prefixDefaultFieldClashes(data)
92+
data["timestamp"] = entry.Time.Format("2006/01/02 15:04:05")
93+
data["level"] = entry.Level.String()
94+
data["message"] = entry.Message
95+
if entry.Caller != nil {
96+
data["caller"] = fmt.Sprintf("%s:%d", shortPath(entry.Caller.File), entry.Caller.Line)
97+
}
98+
99+
var b *bytes.Buffer
100+
if entry.Buffer != nil {
101+
b = entry.Buffer
102+
} else {
103+
b = &bytes.Buffer{}
104+
}
105+
106+
encoder := json.NewEncoder(b)
107+
encoder.SetEscapeHTML(true)
108+
if err := encoder.Encode(data); err != nil {
109+
return nil, fmt.Errorf("failed to marshal fields to JSON: %w", err)
110+
}
111+
112+
return b.Bytes(), nil
113+
}
114+
115+
// prefixDefaultFieldClashes adds a prefix to the keys in data that clash
116+
// with the keys in DefaultJSONFieldKeys to prevent them from being overwritten.
117+
func prefixDefaultFieldClashes(data logrus.Fields) {
118+
for _, fieldKey := range DefaultJSONFieldKeys {
119+
if _, ok := data[fieldKey]; ok {
120+
data["fields."+fieldKey] = data[fieldKey]
121+
// Delete the original non-prefixed key.
122+
delete(data, fieldKey)
123+
}
124+
}
125+
}

log/log.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ import (
1212
"github.com/sirupsen/logrus"
1313
)
1414

15-
var (
16-
Log Logger
17-
)
15+
var Log Logger
1816

1917
type LogOptions struct {
2018
LogLevel string
2119
BugsnagKey string
2220
AppVersion string
21+
22+
// UseJSONFormatter will use JSONFormatter, default is ConsoleFormatter.
23+
UseJSONFormatter bool
2324
}
2425

2526
func InitLog(opts *LogOptions) {
@@ -33,14 +34,18 @@ func InitLog(opts *LogOptions) {
3334
}
3435
}
3536

36-
Log.SetFormatter(&ConsoleFormatter{})
37-
3837
Log.logger.AddHook(&CallerHook{})
3938

4039
if opts == nil {
4140
return
4241
}
4342

43+
if opts.UseJSONFormatter {
44+
Log.SetFormatter(&JSONFormatter{})
45+
} else {
46+
Log.SetFormatter(&ConsoleFormatter{})
47+
}
48+
4449
if opts.LogLevel != "" {
4550
lvl, err := logrus.ParseLevel(opts.LogLevel)
4651
if err == nil {
@@ -74,40 +79,47 @@ func InitLog(opts *LogOptions) {
7479
func WithField(key string, value interface{}) *logrus.Entry {
7580
return Log.WithField(key, value)
7681
}
82+
7783
func WithFields(fields logrus.Fields) *logrus.Entry {
7884
return Log.WithFields(fields)
7985
}
8086

8187
func Debug(args ...interface{}) {
8288
Log.Debug(args...)
8389
}
90+
8491
func Debugf(format string, args ...interface{}) {
8592
Log.Debugf(format, args...)
8693
}
8794

8895
func Info(args ...interface{}) {
8996
Log.Info(args...)
9097
}
98+
9199
func Infof(format string, args ...interface{}) {
92100
Log.Infof(format, args...)
93101
}
94102

95103
func Warning(args ...interface{}) {
96104
Log.WithFields(getSaaskitError(args, 1)).Warning(args...)
97105
}
106+
98107
func Warningf(format string, args ...interface{}) {
99108
Log.WithFields(getSaaskitErrorf(format, args, 1)).Warningf(format, args...)
100109
}
110+
101111
func Warn(args ...interface{}) {
102112
Log.WithFields(getSaaskitError(args, 1)).Warning(args...)
103113
}
114+
104115
func Warnf(format string, args ...interface{}) {
105116
Log.WithFields(getSaaskitErrorf(format, args, 1)).Warningf(format, args...)
106117
}
107118

108119
func Error(args ...interface{}) {
109120
Log.WithFields(getSaaskitError(args, 1)).Error(args...)
110121
}
122+
111123
func Errorf(format string, args ...interface{}) {
112124
// NOTE: this must support the %w wrap verb since vandoor uses it
113125
err := fmt.Errorf(format, args...)
@@ -117,6 +129,7 @@ func Errorf(format string, args ...interface{}) {
117129
func Fatal(args ...interface{}) {
118130
Log.WithFields(getSaaskitError(args, 1)).Fatal(args...)
119131
}
132+
120133
func Fatalf(format string, args ...interface{}) {
121134
Log.WithFields(getSaaskitErrorf(format, args, 1)).Fatalf(format, args...)
122135
}

log/log_test.go

Lines changed: 97 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,60 @@ func TestFilterEvents(t *testing.T) {
4343

4444
func TestCallerHook(t *testing.T) {
4545
param.Init(nil)
46-
47-
h := &CallerHook{}
46+
log := newLogger()
47+
log.AddHook(&CallerHook{})
4848

4949
out := bytes.NewBuffer(nil)
50-
log := newLogger()
5150
log.SetOutput(out)
52-
log.logger.AddHook(h)
53-
log.SetFormatter(&ConsoleFormatter{})
5451

55-
log.Error("test 1")
56-
assert.Contains(t, out.String(), "testing.go:")
52+
validateFunc := func(formatter logrus.Formatter) {
53+
if _, ok := formatter.(*JSONFormatter); ok {
54+
assert.Contains(t, out.String(), "caller", "testing.go:")
55+
} else {
56+
assert.Contains(t, out.String(), "testing.go:")
57+
}
5758

58-
out = bytes.NewBuffer(nil)
59-
log.SetOutput(out)
59+
out.Reset()
60+
}
61+
62+
for _, formatter := range []logrus.Formatter{
63+
&ConsoleFormatter{}, &JSONFormatter{},
64+
} {
65+
t.Run(fmt.Sprintf("%T", formatter), func(t *testing.T) {
66+
log.SetFormatter(formatter)
67+
68+
log.Error("test 1")
69+
validateFunc(formatter)
70+
71+
log.WithField("test", "test").WithField("test2", "test2").Info("test 2")
72+
validateFunc(formatter)
73+
})
74+
}
75+
}
76+
77+
func TestPrefixFieldClashes(t *testing.T) {
78+
param.Init(nil)
79+
Log = newLogger()
80+
81+
out := bytes.NewBuffer(nil)
82+
Log.SetOutput(out)
83+
Log.SetFormatter(&JSONFormatter{})
6084

61-
log.WithField("test", "test").WithField("test2", "test2").Error("test 2")
62-
assert.Contains(t, out.String(), "testing.go:")
85+
Log.AddHook(&CallerHook{})
86+
87+
Log.WithFields(logrus.Fields{
88+
"level": "test",
89+
"message": "test",
90+
"timestamp": "test",
91+
"caller": "test",
92+
}).Info("super awesome test")
93+
94+
assert.Contains(t, out.String(), `"fields.level":"test"`)
95+
assert.Contains(t, out.String(), `"fields.message":"test"`)
96+
assert.Contains(t, out.String(), `"fields.timestamp":"test"`)
97+
assert.Contains(t, out.String(), `"fields.caller":"test"`)
98+
assert.Contains(t, out.String(), `"level":"info"`)
99+
assert.Contains(t, out.String(), `"message":"super awesome test"`)
63100
}
64101

65102
func TestSaaskitError(t *testing.T) {
@@ -85,25 +122,30 @@ func TestSaaskitError(t *testing.T) {
85122
},
86123
}
87124
for _, tt := range tests {
88-
t.Run(tt.name, func(t *testing.T) {
89-
out := bytes.NewBuffer(nil)
90-
Log.SetOutput(out)
91-
92-
h := &hook{}
93-
Log.AddHook(h)
94-
95-
Error(tt.args...)
96-
97-
require.Len(t, h.entries, 1)
98-
require.Contains(t, h.entries[0].Data, "saaskit.error")
99-
if bugsnagErr, ok := h.entries[0].Data["saaskit.error"].(*errors.Error); ok {
100-
assert.IsType(t, tt.wantErrType, bugsnagErr.Err)
101-
firstLine := strings.Split(string(bugsnagErr.Stack()), "\n")[0]
102-
assert.Contains(t, firstLine, "log_test.go:")
103-
} else {
104-
assert.IsType(t, tt.wantErrType, h.entries[0].Data["saaskit.error"])
105-
}
106-
})
125+
for _, formatter := range []logrus.Formatter{
126+
&ConsoleFormatter{}, &JSONFormatter{},
127+
} {
128+
t.Run(fmt.Sprintf("%s %T", tt.name, formatter), func(t *testing.T) {
129+
out := bytes.NewBuffer(nil)
130+
Log.SetOutput(out)
131+
Log.SetFormatter(formatter)
132+
133+
h := &hook{}
134+
Log.AddHook(h)
135+
136+
Error(tt.args...)
137+
138+
require.Len(t, h.entries, 1)
139+
require.Contains(t, h.entries[0].Data, "saaskit.error")
140+
if bugsnagErr, ok := h.entries[0].Data["saaskit.error"].(*errors.Error); ok {
141+
assert.IsType(t, tt.wantErrType, bugsnagErr.Err)
142+
firstLine := strings.Split(string(bugsnagErr.Stack()), "\n")[0]
143+
assert.Contains(t, firstLine, "log_test.go:")
144+
} else {
145+
assert.IsType(t, tt.wantErrType, h.entries[0].Data["saaskit.error"])
146+
}
147+
})
148+
}
107149
}
108150
}
109151

@@ -127,25 +169,30 @@ func TestSaaskitErrorf(t *testing.T) {
127169
},
128170
}
129171
for _, tt := range tests {
130-
t.Run(tt.name, func(t *testing.T) {
131-
out := bytes.NewBuffer(nil)
132-
Log.SetOutput(out)
133-
134-
h := &hook{}
135-
Log.AddHook(h)
136-
137-
Errorf(tt.format, tt.args...)
138-
139-
require.Len(t, h.entries, 1)
140-
require.Contains(t, h.entries[0].Data, "saaskit.error")
141-
if bugsnagErr, ok := h.entries[0].Data["saaskit.error"].(*errors.Error); ok {
142-
assert.IsType(t, tt.wantErrType, bugsnagErr.Err)
143-
firstLine := strings.Split(string(bugsnagErr.Stack()), "\n")[0]
144-
assert.Contains(t, firstLine, "log_test.go:")
145-
} else {
146-
assert.IsType(t, tt.wantErrType, h.entries[0].Data["saaskit.error"])
147-
}
148-
})
172+
for _, formatter := range []logrus.Formatter{
173+
&ConsoleFormatter{}, &JSONFormatter{},
174+
} {
175+
t.Run(fmt.Sprintf("%s %T", tt.name, formatter), func(t *testing.T) {
176+
out := bytes.NewBuffer(nil)
177+
Log.SetOutput(out)
178+
Log.SetFormatter(formatter)
179+
180+
h := &hook{}
181+
Log.AddHook(h)
182+
183+
Errorf(tt.format, tt.args...)
184+
185+
require.Len(t, h.entries, 1)
186+
require.Contains(t, h.entries[0].Data, "saaskit.error")
187+
if bugsnagErr, ok := h.entries[0].Data["saaskit.error"].(*errors.Error); ok {
188+
assert.IsType(t, tt.wantErrType, bugsnagErr.Err)
189+
firstLine := strings.Split(string(bugsnagErr.Stack()), "\n")[0]
190+
assert.Contains(t, firstLine, "log_test.go:")
191+
} else {
192+
assert.IsType(t, tt.wantErrType, h.entries[0].Data["saaskit.error"])
193+
}
194+
})
195+
}
149196
}
150197
}
151198

@@ -170,8 +217,7 @@ func (h *hook) Levels() []logrus.Level {
170217

171218
var _ error = myError{}
172219

173-
type myError struct {
174-
}
220+
type myError struct{}
175221

176222
func (e myError) Error() string {
177223
return "my error"

0 commit comments

Comments
 (0)