Skip to content

Commit 7fccfac

Browse files
committed
Add JSON logger
Closes #10
1 parent 6b0a474 commit 7fccfac

File tree

12 files changed

+240
-38
lines changed

12 files changed

+240
-38
lines changed

examples_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package slog_test
22

33
import (
4+
"context"
45
"io"
6+
"os"
57
"testing"
68

79
"golang.org/x/xerrors"
810

911
"go.coder.com/slog"
12+
"go.coder.com/slog/sloggers/slogjson"
1013
"go.coder.com/slog/sloggers/slogtest"
1114
)
1215

@@ -56,3 +59,19 @@ func TestExample(t *testing.T) {
5659
slog.Component("test"),
5760
)
5861
}
62+
63+
func TestJSON(t *testing.T) {
64+
l := slogjson.Make(os.Stderr)
65+
l.Info(context.Background(), "my message here",
66+
slog.F("field_name", "something or the other"),
67+
slog.F("some_map", slog.Map(
68+
slog.F("nested_fields", "wowow"),
69+
)),
70+
slog.Error(
71+
xerrors.Errorf("wrap1: %w",
72+
xerrors.Errorf("wrap2: %w",
73+
io.EOF),
74+
)),
75+
slog.Component("test"),
76+
)
77+
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/fatih/camelcase v1.0.0
77
github.com/fatih/color v1.7.0
88
github.com/golang/protobuf v1.3.2
9-
github.com/google/go-cmp v0.3.0
9+
github.com/google/go-cmp v0.3.2-0.20190829225427-b1c9c4891a65
1010
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
1111
github.com/kr/pretty v0.1.0 // indirect
1212
github.com/mattn/go-colorable v0.1.2 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
2424
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
2525
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
2626
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
27+
github.com/google/go-cmp v0.3.2-0.20190829225427-b1c9c4891a65 h1:B3yqxlLHBEoav+FDQM8ph7IIRA6AhQ70w119k3hoT2Y=
28+
github.com/google/go-cmp v0.3.2-0.20190829225427-b1c9c4891a65/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
2729
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
2830
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
2931
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=

internal/diff/diff.go renamed to internal/assert/equalf.go

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1-
// Package diff is a helper package around go-cmp for generating
2-
// diffs between arbitrary interfaces.
3-
// See https://github.com/google/go-cmp/issues/40#issuecomment-328615283
4-
// Copied from https://github.com/nhooyr/websocket/blob/1b874731eab56c69c8bb3ebf8a029020c7863fc9/cmp_test.go
5-
package diff
1+
// Package assert is a helper package for asserting equality and
2+
// generating diffs in tests.
3+
package assert
64

75
import (
86
"reflect"
7+
"testing"
98

109
"github.com/google/go-cmp/cmp"
1110
)
1211

13-
// Diff returns a diff between exp and act.
12+
// Equalf compares exp to act. If they are not equal, it will fatal the test
13+
// with the passed msg, args and also a diff of the differences.
14+
func Equalf(t *testing.T, exp, act interface{}, msg string, v ...interface{}) {
15+
if diff := diff(exp, act); diff != "" {
16+
t.Fatalf(msg+": %v", append(v, diff)...)
17+
}
18+
}
19+
20+
// diff returns a diff between exp and act.
1421
// The empty string is returned if they are identical.
15-
func Diff(exp, act interface{}) string {
22+
// See https://github.com/google/go-cmp/issues/40#issuecomment-328615283
23+
// Copied from https://github.com/nhooyr/websocket/blob/1b874731eab56c69c8bb3ebf8a029020c7863fc9/cmp_test.go
24+
func diff(exp, act interface{}) string {
1625
return cmp.Diff(exp, act, deepAllowUnexported(exp, act))
1726
}
1827

internal/humanfmt/marshal_test.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"testing"
55

66
"go.coder.com/slog"
7-
"go.coder.com/slog/internal/diff"
7+
"go.coder.com/slog/internal/assert"
88
"go.coder.com/slog/slogval"
99
)
1010

@@ -107,6 +107,17 @@ c:`,
107107
dsamkld`),
108108
),
109109
out: `"nhooyr_\tsoftware™️": hi
110+
"\rxeow\r": mdsla
111+
dsamkld`,
112+
},
113+
{
114+
name: "specialCharacterKey",
115+
in: slog.Map(
116+
slog.F("nhooyr \tsoftware™️", "hi"),
117+
slog.F("\rxeow\r", `mdsla
118+
dsamkld`),
119+
),
120+
out: `"nhooyr_\tsoftware™️": hi
110121
"\rxeow\r": mdsla
111122
dsamkld`,
112123
},
@@ -120,9 +131,7 @@ dsamkld`),
120131
v := slogval.Encode(tc.in).(slogval.Map)
121132
actOut := humanFields(v)
122133
t.Logf("yaml:\n%v", actOut)
123-
if diff := diff.Diff(tc.out, actOut); diff != "" {
124-
t.Fatalf("unexpected output: %v", diff)
125-
}
134+
assert.Equalf(t, tc.out, actOut, "unexpected output")
126135
})
127136
}
128137
}

internal/syncwriter/syncwriter.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Package syncwriter implements a concurrency safe io.Writer wrapper.
2+
package syncwriter
3+
4+
import (
5+
"io"
6+
"sync"
7+
)
8+
9+
// Writer implements a concurrency safe io.Writer wrapper.
10+
type Writer struct {
11+
mu sync.Mutex
12+
w io.Writer
13+
}
14+
15+
// New returns a new Writer that writes to w.
16+
func New(w io.Writer) *Writer {
17+
return &Writer{
18+
w: w,
19+
}
20+
}
21+
22+
// Write implements io.Writer.
23+
func (w *Writer) Write(p []byte) (int, error) {
24+
w.mu.Lock()
25+
defer w.mu.Unlock()
26+
return w.w.Write(p)
27+
}

sloggers/sloghuman/sloghuman.go

+9-16
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,42 @@ import (
66
"context"
77
"io"
88
"strings"
9-
"sync"
109

1110
"go.coder.com/slog"
1211
"go.coder.com/slog/internal/humanfmt"
12+
"go.coder.com/slog/internal/syncwriter"
1313
)
1414

1515
type humanSink struct {
16-
mu sync.Mutex
17-
w io.Writer
16+
w *syncwriter.Writer
1817
color bool
1918
}
2019

21-
var _ slog.Sink = &humanSink{}
22-
23-
func (s *humanSink) LogEntry(ctx context.Context, ent slog.Entry) {
20+
func (s humanSink) LogEntry(ctx context.Context, ent slog.Entry) {
2421
str := humanfmt.Entry(ent, s.color)
2522
lines := strings.Split(str, "\n")
2623

24+
// We need to add 4 spaces before every field line for readability.
25+
// humanfmt doesn't do it for us because the testSink doesn't want
26+
// it as *testing.T automatically does it.
2727
fieldsLines := lines[1:]
2828
for i, line := range fieldsLines {
2929
if line == "" {
3030
continue
3131
}
32-
fieldsLines[i] = "\t" + line
32+
fieldsLines[i] = strings.Repeat(" ", 4) + line
3333
}
3434

3535
str = strings.Join(lines, "\n")
3636

37-
s.writeString(str + "\n")
38-
}
39-
40-
func (s *humanSink) writeString(str string) {
41-
s.mu.Lock()
42-
defer s.mu.Unlock()
43-
44-
io.WriteString(s.w, str)
37+
io.WriteString(s.w, str+"\n")
4538
}
4639

4740
// Make creates a logger that writes logs in a human
4841
// readable YAML like format to the given writer.
4942
func Make(w io.Writer) slog.Logger {
5043
return slog.Make(&humanSink{
51-
w: w,
44+
w: syncwriter.New(w),
5245
color: humanfmt.IsTTY(w),
5346
})
5447
}

sloggers/slogjson/slogjson.go

+71-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,82 @@
1-
// Package slogjson contains the slogger
2-
// that writes logs in a JSON format.
1+
// Package slogjson contains the slogger that writes logs in a JSON format.
2+
//
3+
// Format
4+
//
5+
// {
6+
// "level": "INFO",
7+
// "msg": "hi",
8+
// "ts": "",
9+
// "caller": "slog/examples_test.go:62",
10+
// "func": "go.coder.com/slog/sloggers/slogtest_test.TestExampleTest",
11+
// "component": "comp.subcomp",
12+
// "trace": "<traceid>",
13+
// "span": "<spanid>",
14+
// "fields": {
15+
// "myField": "fieldValue"
16+
// }
17+
// }
318
package slogjson // import "go.coder.com/slog/sloggers/slogjson"
419

520
import (
21+
"context"
22+
"encoding/json"
23+
"fmt"
624
"io"
25+
"time"
26+
27+
"go.opencensus.io/trace"
28+
"golang.org/x/xerrors"
729

830
"go.coder.com/slog"
31+
"go.coder.com/slog/internal/syncwriter"
32+
"go.coder.com/slog/slogval"
933
)
1034

1135
// Make creates a logger that writes JSON logs
12-
// to the given writer. The format is as follows:
36+
// to the given writer. See package level docs
37+
// for the format.
1338
func Make(w io.Writer) slog.Logger {
14-
panic("TODO")
39+
return slog.Make(jsonSink{
40+
w: syncwriter.New(w),
41+
})
42+
}
43+
44+
type jsonSink struct {
45+
w *syncwriter.Writer
46+
}
47+
48+
func (s jsonSink) LogEntry(ctx context.Context, ent slog.Entry) {
49+
m := slog.Map(
50+
slog.F("level", ent.Level),
51+
slog.F("msg", ent.Message),
52+
slog.F("ts", jsonTimestamp(ent.Time)),
53+
slog.F("caller", fmt.Sprintf("%v:%v", ent.File, ent.Line)),
54+
slog.F("func", ent.Func),
55+
slog.F("component", ent.Component),
56+
)
57+
58+
if ent.SpanContext != (trace.SpanContext{}) {
59+
m = append(m,
60+
slog.F("trace", ent.SpanContext.TraceID),
61+
slog.F("span", ent.SpanContext.SpanID),
62+
)
63+
}
64+
65+
m = append(m,
66+
slog.F("fields", ent.Fields),
67+
)
68+
69+
v := slogval.Reflect(m)
70+
// We use NewEncoder because it reuses buffers behind the scenes which we cannot
71+
// do with json.Marshal.
72+
e := json.NewEncoder(s.w)
73+
e.Encode(v)
74+
}
75+
76+
func jsonTimestamp(t time.Time) interface{} {
77+
ts, err := t.MarshalText()
78+
if err != nil {
79+
return xerrors.Errorf("failed to marshal timestamp to text: %w", err)
80+
}
81+
return string(ts)
1582
}

slogval/encode.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func encode(rv reflect.Value) Value {
5454
lv := m.Call(nil)
5555
return encode(lv[0])
5656
case implements(typ, (*xerrors.Formatter)(nil)):
57-
return extractErrorChain(rv)
57+
return extractXErrorChain(rv)
5858
case implements(typ, (*error)(nil)):
5959
m := rv.MethodByName("Error")
6060
s := m.Call(nil)
@@ -231,7 +231,7 @@ func (p *xerrorPrinter) fields() Value {
231231
}
232232

233233
// The value passed in must implement xerrors.Formatter.
234-
func extractErrorChain(rv reflect.Value) List {
234+
func extractXErrorChain(rv reflect.Value) List {
235235
errs := List{}
236236

237237
formatError := func(p xerrors.Printer) error {

slogval/encode_test.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88

99
"golang.org/x/xerrors"
1010

11-
"go.coder.com/slog/internal/diff"
11+
"go.coder.com/slog/internal/assert"
1212
)
1313

1414
func TestEncode(t *testing.T) {
@@ -75,9 +75,7 @@ go.coder.com/slog/slogval.TestEncode
7575
t.Parallel()
7676

7777
actOut := Encode(tc.in)
78-
if diff := diff.Diff(tc.out, actOut); diff != "" {
79-
t.Fatalf("unexpected output: %v", diff)
80-
}
78+
assert.Equalf(t, tc.out, actOut, "unexpected output")
8179
})
8280
}
8381
}

slogval/slogval.go

+35
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
package slogval // import "go.coder.com/slog/slogval"
44

55
import (
6+
"bytes"
7+
"encoding/json"
68
"sort"
9+
10+
"golang.org/x/xerrors"
711
)
812

913
// Value represents a primitive value for structured logging.
@@ -87,3 +91,34 @@ func (m Map) sort() {
8791
return m[i].Name < m[j].Name
8892
})
8993
}
94+
95+
var _ json.Marshaler = Map(nil)
96+
97+
// MarshalJSON implements json.Marshaler.
98+
func (m Map) MarshalJSON() ([]byte, error) {
99+
b := &bytes.Buffer{}
100+
b.WriteString("{")
101+
for i, f := range m {
102+
fieldName, err := json.Marshal(f.Name)
103+
if err != nil {
104+
return nil, xerrors.Errorf("failed to marshal field name: %w", err)
105+
}
106+
107+
fieldValue, err := json.Marshal(f.Value)
108+
if err != nil {
109+
return nil, xerrors.Errorf("failed to marshal field value: %w", err)
110+
}
111+
112+
b.WriteString("\n")
113+
b.Write(fieldName)
114+
b.WriteString(":")
115+
b.Write(fieldValue)
116+
117+
if i < len(m)-1 {
118+
b.WriteString(",")
119+
}
120+
}
121+
b.WriteString(`}`)
122+
123+
return b.Bytes(), nil
124+
}

0 commit comments

Comments
 (0)