Add StdlibWriter adapter
- Allows Loggers to be passed to log.SetOutput
- Extracts date, time, file, message keyvals
- I have no idea how to support SetPrefix :(
Peter Bourgon
9 years ago
0 | package log | |
1 | ||
2 | import ( | |
3 | "io" | |
4 | "regexp" | |
5 | ) | |
6 | ||
7 | // StdlibWriter wraps a Logger and allows it to be passed to the stdlib | |
8 | // logger's SetOutput. It will extract date/timestamps, filenames, and | |
9 | // messages, and place them under relevant keys. | |
10 | type StdlibWriter struct { | |
11 | Logger | |
12 | timestampKey string | |
13 | fileKey string | |
14 | messageKey string | |
15 | } | |
16 | ||
17 | // StdlibWriterOption sets a parameter for the StdlibWriter. | |
18 | type StdlibWriterOption func(*StdlibWriter) | |
19 | ||
20 | // TimestampKey sets the key for the timestamp field. By default, it's "ts". | |
21 | func TimestampKey(key string) StdlibWriterOption { | |
22 | return func(w *StdlibWriter) { w.timestampKey = key } | |
23 | } | |
24 | ||
25 | // FileKey sets the key for the file and line field. By default, it's "file". | |
26 | func FileKey(key string) StdlibWriterOption { | |
27 | return func(w *StdlibWriter) { w.fileKey = key } | |
28 | } | |
29 | ||
30 | // MessageKey sets the key for the actual log message. By default, it's "msg". | |
31 | func MessageKey(key string) StdlibWriterOption { | |
32 | return func(w *StdlibWriter) { w.messageKey = key } | |
33 | } | |
34 | ||
35 | // NewStdlibWriter returns a new StdlibWriter wrapper around the passed | |
36 | // logger. It's designed to be passed to log.SetOutput. | |
37 | func NewStdlibWriter(logger Logger, options ...StdlibWriterOption) io.Writer { | |
38 | w := StdlibWriter{ | |
39 | Logger: logger, | |
40 | timestampKey: "ts", | |
41 | fileKey: "file", | |
42 | messageKey: "msg", | |
43 | } | |
44 | for _, option := range options { | |
45 | option(&w) | |
46 | } | |
47 | return w | |
48 | } | |
49 | ||
50 | func (w StdlibWriter) Write(p []byte) (int, error) { | |
51 | result := subexps(p) | |
52 | keyvals := []interface{}{} | |
53 | var timestamp string | |
54 | if date, ok := result["date"]; ok && date != "" { | |
55 | timestamp = date | |
56 | } | |
57 | if time, ok := result["time"]; ok && time != "" { | |
58 | if timestamp != "" { | |
59 | timestamp += " " | |
60 | } | |
61 | timestamp += time | |
62 | } | |
63 | if timestamp != "" { | |
64 | keyvals = append(keyvals, w.timestampKey, timestamp) | |
65 | } | |
66 | if file, ok := result["file"]; ok && file != "" { | |
67 | keyvals = append(keyvals, w.fileKey, file) | |
68 | } | |
69 | if msg, ok := result["msg"]; ok { | |
70 | keyvals = append(keyvals, w.messageKey, msg) | |
71 | } | |
72 | if err := w.Logger.Log(keyvals...); err != nil { | |
73 | return 0, err | |
74 | } | |
75 | return len(p), nil | |
76 | } | |
77 | ||
78 | const ( | |
79 | logRegexpDate = `(?P<date>[0-9]{4}/[0-9]{2}/[0-9]{2})?[ ]?` | |
80 | logRegexpTime = `(?P<time>[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?)?[ ]?` | |
81 | logRegexpFile = `(?P<file>[^:]+:[0-9]+)?` | |
82 | logRegexpMsg = `(: )?(?P<msg>.*)` | |
83 | ) | |
84 | ||
85 | var ( | |
86 | logRegexp = regexp.MustCompile(logRegexpDate + logRegexpTime + logRegexpFile + logRegexpMsg) | |
87 | ) | |
88 | ||
89 | func subexps(line []byte) map[string]string { | |
90 | m := logRegexp.FindSubmatch(line) | |
91 | if len(m) < len(logRegexp.SubexpNames()) { | |
92 | return map[string]string{} | |
93 | } | |
94 | result := map[string]string{} | |
95 | for i, name := range logRegexp.SubexpNames() { | |
96 | result[name] = string(m[i]) | |
97 | } | |
98 | return result | |
99 | } |
0 | package log | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "fmt" | |
5 | "testing" | |
6 | ) | |
7 | ||
8 | func TestStdLibWriterExtraction(t *testing.T) { | |
9 | buf := &bytes.Buffer{} | |
10 | logger := NewPrefixLogger(buf) | |
11 | writer := NewStdlibWriter(logger) | |
12 | for input, want := range map[string]string{ | |
13 | "hello": "msg=hello\n", | |
14 | "2009/01/23: hello": "ts=2009/01/23 msg=hello\n", | |
15 | "2009/01/23 01:23:23: hello": "ts=2009/01/23 01:23:23 msg=hello\n", | |
16 | "01:23:23: hello": "ts=01:23:23 msg=hello\n", | |
17 | "2009/01/23 01:23:23.123123: hello": "ts=2009/01/23 01:23:23.123123 msg=hello\n", | |
18 | "2009/01/23 01:23:23.123123 /a/b/c/d.go:23: hello": "ts=2009/01/23 01:23:23.123123 file=/a/b/c/d.go:23 msg=hello\n", | |
19 | "01:23:23.123123 /a/b/c/d.go:23: hello": "ts=01:23:23.123123 file=/a/b/c/d.go:23 msg=hello\n", | |
20 | "2009/01/23 01:23:23 /a/b/c/d.go:23: hello": "ts=2009/01/23 01:23:23 file=/a/b/c/d.go:23 msg=hello\n", | |
21 | "2009/01/23 /a/b/c/d.go:23: hello": "ts=2009/01/23 file=/a/b/c/d.go:23 msg=hello\n", | |
22 | "/a/b/c/d.go:23: hello": "file=/a/b/c/d.go:23 msg=hello\n", | |
23 | } { | |
24 | buf.Reset() | |
25 | fmt.Fprintf(writer, input) | |
26 | if have := buf.String(); want != have { | |
27 | t.Errorf("%q: want %#v, have %#v", input, want, have) | |
28 | } | |
29 | } | |
30 | } | |
31 | ||
32 | func TestStdlibWriterSubexps(t *testing.T) { | |
33 | for input, wantMap := range map[string]map[string]string{ | |
34 | "hello world": map[string]string{ | |
35 | "date": "", | |
36 | "time": "", | |
37 | "file": "", | |
38 | "msg": "hello world", | |
39 | }, | |
40 | "2009/01/23: hello world": map[string]string{ | |
41 | "date": "2009/01/23", | |
42 | "time": "", | |
43 | "file": "", | |
44 | "msg": "hello world", | |
45 | }, | |
46 | "2009/01/23 01:23:23: hello world": map[string]string{ | |
47 | "date": "2009/01/23", | |
48 | "time": "01:23:23", | |
49 | "file": "", | |
50 | "msg": "hello world", | |
51 | }, | |
52 | "01:23:23: hello world": map[string]string{ | |
53 | "date": "", | |
54 | "time": "01:23:23", | |
55 | "file": "", | |
56 | "msg": "hello world", | |
57 | }, | |
58 | "2009/01/23 01:23:23.123123: hello world": map[string]string{ | |
59 | "date": "2009/01/23", | |
60 | "time": "01:23:23.123123", | |
61 | "file": "", | |
62 | "msg": "hello world", | |
63 | }, | |
64 | "2009/01/23 01:23:23.123123 /a/b/c/d.go:23: hello world": map[string]string{ | |
65 | "date": "2009/01/23", | |
66 | "time": "01:23:23.123123", | |
67 | "file": "/a/b/c/d.go:23", | |
68 | "msg": "hello world", | |
69 | }, | |
70 | "01:23:23.123123 /a/b/c/d.go:23: hello world": map[string]string{ | |
71 | "date": "", | |
72 | "time": "01:23:23.123123", | |
73 | "file": "/a/b/c/d.go:23", | |
74 | "msg": "hello world", | |
75 | }, | |
76 | "2009/01/23 01:23:23 /a/b/c/d.go:23: hello world": map[string]string{ | |
77 | "date": "2009/01/23", | |
78 | "time": "01:23:23", | |
79 | "file": "/a/b/c/d.go:23", | |
80 | "msg": "hello world", | |
81 | }, | |
82 | "2009/01/23 /a/b/c/d.go:23: hello world": map[string]string{ | |
83 | "date": "2009/01/23", | |
84 | "time": "", | |
85 | "file": "/a/b/c/d.go:23", | |
86 | "msg": "hello world", | |
87 | }, | |
88 | "/a/b/c/d.go:23: hello world": map[string]string{ | |
89 | "date": "", | |
90 | "time": "", | |
91 | "file": "/a/b/c/d.go:23", | |
92 | "msg": "hello world", | |
93 | }, | |
94 | } { | |
95 | haveMap := subexps([]byte(input)) | |
96 | for key, want := range wantMap { | |
97 | if have := haveMap[key]; want != have { | |
98 | t.Errorf("%q: %q: want %q, have %q", input, key, want, have) | |
99 | } | |
100 | } | |
101 | } | |
102 | } |