-
Notifications
You must be signed in to change notification settings - Fork 1
/
logging.go
133 lines (112 loc) · 3.84 KB
/
logging.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
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" // nolint:depguard
"golang.org/x/exp/slices"
)
func configureLogging(cfg *config) error {
parsed, err := zerolog.ParseLevel(strings.ToLower(cfg.LogLevel))
if err != nil {
return fmt.Errorf("invalid log level: %s", cfg.LogLevel)
}
zerolog.SetGlobalLevel(parsed)
var output io.Writer = messageEnhancerWriter{os.Stderr}
if !cfg.JsonLog {
output = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.Out = os.Stderr
})
}
l := log.Output(output)
log.Logger = l
// default is with whole second precision
zerolog.TimeFieldFormat = time.RFC3339Nano
zerolog.DefaultContextLogger = &l
return nil
}
// We want to achieve two different goals when writing logs:
// 1. Structured logging - i.e. level, timestamp field, metadata. That way
// our log aggregator can turn our logs into meaningful data.
// 2. It should still be human-readable. If the structured log aggregator
// only shows us the `message` field, we'll lose a bunch of value
// information.
//
// We do this by monkey-patching the `message` field with the other fields
// we want to see. This involves a bit of back-and-forth JSON parsing and
// manipulating, but we aren't exactly implementing performance critical
// software anyway.
type messageEnhancerWriter struct {
underlying io.Writer
}
// These are fields we aren't interested in adding to the `message` field.
var uninterestingFields = []string{
"message", // The actual field we're manipulating - don't want to duplicate.
"level", // Added automatically by the log aggregator.
"time", // Added automatically by the log aggregator.
"caller", // Very noisy. If we're interested in a specific line, we can still find this.
"commit",
}
// Write implements io.Writer
func (m messageEnhancerWriter) Write(p []byte) (int, error) {
// The log line is a JSON blob which we want to manipulate a little
// before passing on. We're going to parse it into this map.
var evt map[string]any
// First, do the parsing.
dec := json.NewDecoder(bytes.NewReader(p))
// JSON numbers are notoriously finicky, use this safer string-based
// representation
dec.UseNumber()
if err := dec.Decode(&evt); err != nil {
return 0, fmt.Errorf("decode event: %w", err)
}
// This is the value we're going to enhance, before sticking it back in.
message, ok := evt["message"]
if !ok {
message = ""
evt["message"] = message
}
// Now we'll add all our fields to the message, for human consumption.
for key, value := range evt {
if value == nil {
continue
}
// Skip all the boring fields.
if slices.Contains(uninterestingFields, key) {
continue
}
// We want to present durations as human-readable fields. Conversions
// done with the `.Dur` method in zerolog converts the value to a
// json.Number, so we can't check for a time.Duration type.
// Therefore, check for a json.Number, and a few well known fields
// that contain durations. This just affects the manipulated `message`
// field, the raw values are still present in the JSON object.
if fValue, ok := value.(json.Number); ok && key == "duration" {
millis, _ := fValue.Float64()
duration := time.Duration(millis) * time.Millisecond
// convert the value to a human-readable representation
value = duration.String()
}
if boolValue, ok := value.(bool); ok {
value = fmt.Sprintf("%t", boolValue)
}
const sep = ":"
message = fmt.Sprintf("%s %s%s%s", message, key, sep, value)
}
// Stick the manipulated message back in.
evt["message"] = message
// Marshal back to JSON
marshaled, err := json.Marshal(evt)
if err != nil {
return 0, err
}
// Be sure to add a newline!
marshaled = append(marshaled, []byte("\n")...)
// And then write to the underlying writer.
return m.underlying.Write(marshaled)
}