Skip to content

Commit be4c235

Browse files
authored
Support customization of timestamp format (#398)
By default, RFC3339 timestamps are used, but our application uses a custom format. This commit enables us to set the format in a consistent manner. Closes #131
1 parent 912313c commit be4c235

12 files changed

+190
-117
lines changed

logging/kit/options.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@ import (
1111

1212
var (
1313
defaultOptions = &options{
14-
shouldLog: grpc_logging.DefaultDeciderMethod,
15-
codeFunc: grpc_logging.DefaultErrorToCode,
16-
durationFunc: DefaultDurationToField,
14+
shouldLog: grpc_logging.DefaultDeciderMethod,
15+
codeFunc: grpc_logging.DefaultErrorToCode,
16+
durationFunc: DefaultDurationToField,
17+
timestampFormat: time.RFC3339,
1718
}
1819
)
1920

2021
type options struct {
21-
levelFunc CodeToLevel
22-
shouldLog grpc_logging.Decider
23-
codeFunc grpc_logging.ErrorToCode
24-
durationFunc DurationToField
22+
levelFunc CodeToLevel
23+
shouldLog grpc_logging.Decider
24+
codeFunc grpc_logging.ErrorToCode
25+
durationFunc DurationToField
26+
timestampFormat string
2527
}
2628

2729
type Option func(*options)
@@ -80,6 +82,13 @@ func WithDurationField(f DurationToField) Option {
8082
}
8183
}
8284

85+
// WithTimestampFormat customizes the timestamps emitted in the log fields.
86+
func WithTimestampFormat(format string) Option {
87+
return func(o *options) {
88+
o.timestampFormat = format
89+
}
90+
}
91+
8392
// DefaultCodeToLevel is the default implementation of gRPC return codes and interceptor log level for server side.
8493
func DefaultCodeToLevel(code codes.Code, logger log.Logger) log.Logger {
8594
switch code {

logging/kit/server_interceptors.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func UnaryServerInterceptor(logger log.Logger, opts ...Option) grpc.UnaryServerI
2525
o := evaluateServerOpt(opts)
2626
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
2727
startTime := time.Now()
28-
newCtx := injectLogger(ctx, logger, info.FullMethod, startTime)
28+
newCtx := injectLogger(ctx, logger, info.FullMethod, startTime, o.timestampFormat)
2929

3030
resp, err := handler(newCtx, req)
3131
if !o.shouldLog(info.FullMethod, err) {
@@ -44,7 +44,7 @@ func StreamServerInterceptor(logger log.Logger, opts ...Option) grpc.StreamServe
4444
o := evaluateServerOpt(opts)
4545
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
4646
startTime := time.Now()
47-
newCtx := injectLogger(stream.Context(), logger, info.FullMethod, startTime)
47+
newCtx := injectLogger(stream.Context(), logger, info.FullMethod, startTime, o.timestampFormat)
4848

4949
wrapped := grpc_middleware.WrapServerStream(stream)
5050
wrapped.WrappedContext = newCtx
@@ -61,11 +61,11 @@ func StreamServerInterceptor(logger log.Logger, opts ...Option) grpc.StreamServe
6161
}
6262
}
6363

64-
func injectLogger(ctx context.Context, logger log.Logger, fullMethodString string, start time.Time) context.Context {
64+
func injectLogger(ctx context.Context, logger log.Logger, fullMethodString string, start time.Time, timestampFormat string) context.Context {
6565
f := ctxkit.TagsToFields(ctx)
66-
f = append(f, "grpc.start_time", start.Format(time.RFC3339))
66+
f = append(f, "grpc.start_time", start.Format(timestampFormat))
6767
if d, ok := ctx.Deadline(); ok {
68-
f = append(f, "grpc.request.deadline", d.Format(time.RFC3339))
68+
f = append(f, "grpc.request.deadline", d.Format(timestampFormat))
6969
}
7070
f = append(f, serverCallFields(fullMethodString)...)
7171
callLog := log.With(logger, f...)

logging/kit/server_interceptors_test.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,34 @@ func TestKitLoggingSuite(t *testing.T) {
3333
t.Skipf("Skipping due to json.RawMessage incompatibility with go1.7")
3434
return
3535
}
36-
opts := []grpc_kit.Option{
37-
grpc_kit.WithLevels(customCodeToLevel),
38-
}
39-
b := newKitBaseSuite(t)
40-
b.InterceptorTestSuite.ServerOpts = []grpc.ServerOption{
41-
grpc_middleware.WithStreamServerChain(
42-
grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
43-
grpc_kit.StreamServerInterceptor(b.logger, opts...)),
44-
grpc_middleware.WithUnaryServerChain(
45-
grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
46-
grpc_kit.UnaryServerInterceptor(b.logger, opts...)),
36+
37+
for _, tcase := range []struct {
38+
timestampFormat string
39+
}{
40+
{
41+
timestampFormat: time.RFC3339,
42+
},
43+
{
44+
timestampFormat: "2006-01-02",
45+
},
46+
} {
47+
opts := []grpc_kit.Option{
48+
grpc_kit.WithLevels(customCodeToLevel),
49+
grpc_kit.WithTimestampFormat(tcase.timestampFormat),
50+
}
51+
52+
b := newKitBaseSuite(t)
53+
b.timestampFormat = tcase.timestampFormat
54+
b.InterceptorTestSuite.ServerOpts = []grpc.ServerOption{
55+
grpc_middleware.WithStreamServerChain(
56+
grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
57+
grpc_kit.StreamServerInterceptor(b.logger, opts...)),
58+
grpc_middleware.WithUnaryServerChain(
59+
grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
60+
grpc_kit.UnaryServerInterceptor(b.logger, opts...)),
61+
}
62+
suite.Run(t, &kitServerSuite{b})
4763
}
48-
suite.Run(t, &kitServerSuite{b})
4964
}
5065

5166
type kitServerSuite struct {
@@ -69,13 +84,13 @@ func (s *kitServerSuite) TestPing_WithCustomTags() {
6984

7085
assert.Contains(s.T(), m, "custom_tags.int", "all lines must contain `custom_tags.int`")
7186
require.Contains(s.T(), m, "grpc.start_time", "all lines must contain the start time")
72-
_, err := time.Parse(time.RFC3339, m["grpc.start_time"].(string))
73-
assert.NoError(s.T(), err, "should be able to parse start time as RFC3339")
87+
_, err := time.Parse(s.timestampFormat, m["grpc.start_time"].(string))
88+
assert.NoError(s.T(), err, "should be able to parse start time")
7489

7590
require.Contains(s.T(), m, "grpc.request.deadline", "all lines must contain the deadline of the call")
76-
_, err = time.Parse(time.RFC3339, m["grpc.request.deadline"].(string))
77-
require.NoError(s.T(), err, "should be able to parse deadline as RFC3339")
78-
assert.Equal(s.T(), m["grpc.request.deadline"], deadline.Format(time.RFC3339), "should have the same deadline that was set by the caller")
91+
_, err = time.Parse(s.timestampFormat, m["grpc.request.deadline"].(string))
92+
require.NoError(s.T(), err, "should be able to parse deadline")
93+
assert.Equal(s.T(), m["grpc.request.deadline"], deadline.Format(s.timestampFormat), "should have the same deadline that was set by the caller")
7994
}
8095

8196
assert.Equal(s.T(), msgs[0]["msg"], "some ping", "handler's message must contain user message")
@@ -129,8 +144,8 @@ func (s *kitServerSuite) TestPingError_WithCustomLevels() {
129144
assert.Equal(s.T(), m["msg"], "finished unary call with code "+tcase.code.String(), "needs the correct end message")
130145

131146
require.Contains(s.T(), m, "grpc.start_time", "all lines must contain the start time")
132-
_, err = time.Parse(time.RFC3339, m["grpc.start_time"].(string))
133-
assert.NoError(s.T(), err, "should be able to parse start time as RFC3339")
147+
_, err = time.Parse(s.timestampFormat, m["grpc.start_time"].(string))
148+
assert.NoError(s.T(), err, "should be able to parse start time")
134149
}
135150
}
136151

@@ -156,8 +171,8 @@ func (s *kitServerSuite) TestPingList_WithCustomTags() {
156171

157172
assert.Contains(s.T(), m, "custom_tags.int", "all lines must contain `custom_tags.int` set by AddFields")
158173
require.Contains(s.T(), m, "grpc.start_time", "all lines must contain the start time")
159-
_, err := time.Parse(time.RFC3339, m["grpc.start_time"].(string))
160-
assert.NoError(s.T(), err, "should be able to parse start time as RFC3339")
174+
_, err := time.Parse(s.timestampFormat, m["grpc.start_time"].(string))
175+
assert.NoError(s.T(), err, "should be able to parse start time")
161176
}
162177

163178
assert.Equal(s.T(), msgs[0]["msg"], "some pinglist", "handler's message must contain user message")

logging/kit/shared_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ func (s *loggingPingService) PingEmpty(ctx context.Context, empty *pb_testproto.
4949

5050
type kitBaseSuite struct {
5151
*grpc_testing.InterceptorTestSuite
52-
mutexBuffer *grpc_testing.MutexReadWriter
53-
buffer *bytes.Buffer
54-
logger log.Logger
52+
mutexBuffer *grpc_testing.MutexReadWriter
53+
buffer *bytes.Buffer
54+
logger log.Logger
55+
timestampFormat string
5556
}
5657

5758
func newKitBaseSuite(t *testing.T) *kitBaseSuite {

logging/logrus/options.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,22 @@ import (
1515

1616
var (
1717
defaultOptions = &options{
18-
levelFunc: nil,
19-
shouldLog: grpc_logging.DefaultDeciderMethod,
20-
codeFunc: grpc_logging.DefaultErrorToCode,
21-
durationFunc: DefaultDurationToField,
22-
messageFunc: DefaultMessageProducer,
18+
levelFunc: nil,
19+
shouldLog: grpc_logging.DefaultDeciderMethod,
20+
codeFunc: grpc_logging.DefaultErrorToCode,
21+
durationFunc: DefaultDurationToField,
22+
messageFunc: DefaultMessageProducer,
23+
timestampFormat: time.RFC3339,
2324
}
2425
)
2526

2627
type options struct {
27-
levelFunc CodeToLevel
28-
shouldLog grpc_logging.Decider
29-
codeFunc grpc_logging.ErrorToCode
30-
durationFunc DurationToField
31-
messageFunc MessageProducer
28+
levelFunc CodeToLevel
29+
shouldLog grpc_logging.Decider
30+
codeFunc grpc_logging.ErrorToCode
31+
durationFunc DurationToField
32+
messageFunc MessageProducer
33+
timestampFormat string
3234
}
3335

3436
func evaluateServerOpt(opts []Option) *options {
@@ -94,6 +96,13 @@ func WithMessageProducer(f MessageProducer) Option {
9496
}
9597
}
9698

99+
// WithTimestampFormat customizes the timestamps emitted in the log fields.
100+
func WithTimestampFormat(format string) Option {
101+
return func(o *options) {
102+
o.timestampFormat = format
103+
}
104+
}
105+
97106
// DefaultCodeToLevel is the default implementation of gRPC return codes to log levels for server side.
98107
func DefaultCodeToLevel(code codes.Code) logrus.Level {
99108
switch code {

logging/logrus/server_interceptors.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func UnaryServerInterceptor(entry *logrus.Entry, opts ...Option) grpc.UnaryServe
2626
o := evaluateServerOpt(opts)
2727
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
2828
startTime := time.Now()
29-
newCtx := newLoggerForCall(ctx, entry, info.FullMethod, startTime)
29+
newCtx := newLoggerForCall(ctx, entry, info.FullMethod, startTime, o.timestampFormat)
3030

3131
resp, err := handler(newCtx, req)
3232

@@ -54,7 +54,7 @@ func StreamServerInterceptor(entry *logrus.Entry, opts ...Option) grpc.StreamSer
5454
o := evaluateServerOpt(opts)
5555
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
5656
startTime := time.Now()
57-
newCtx := newLoggerForCall(stream.Context(), entry, info.FullMethod, startTime)
57+
newCtx := newLoggerForCall(stream.Context(), entry, info.FullMethod, startTime, o.timestampFormat)
5858
wrapped := grpc_middleware.WrapServerStream(stream)
5959
wrapped.WrappedContext = newCtx
6060

@@ -76,7 +76,7 @@ func StreamServerInterceptor(entry *logrus.Entry, opts ...Option) grpc.StreamSer
7676
}
7777
}
7878

79-
func newLoggerForCall(ctx context.Context, entry *logrus.Entry, fullMethodString string, start time.Time) context.Context {
79+
func newLoggerForCall(ctx context.Context, entry *logrus.Entry, fullMethodString string, start time.Time, timestampFormat string) context.Context {
8080
service := path.Dir(fullMethodString)[1:]
8181
method := path.Base(fullMethodString)
8282
callLog := entry.WithFields(
@@ -85,13 +85,13 @@ func newLoggerForCall(ctx context.Context, entry *logrus.Entry, fullMethodString
8585
KindField: "server",
8686
"grpc.service": service,
8787
"grpc.method": method,
88-
"grpc.start_time": start.Format(time.RFC3339),
88+
"grpc.start_time": start.Format(timestampFormat),
8989
})
9090

9191
if d, ok := ctx.Deadline(); ok {
9292
callLog = callLog.WithFields(
9393
logrus.Fields{
94-
"grpc.request.deadline": d.Format(time.RFC3339),
94+
"grpc.request.deadline": d.Format(timestampFormat),
9595
})
9696
}
9797

logging/logrus/server_interceptors_test.go

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,33 @@ import (
1818
)
1919

2020
func TestLogrusServerSuite(t *testing.T) {
21-
opts := []grpc_logrus.Option{
22-
grpc_logrus.WithLevels(customCodeToLevel),
23-
}
24-
b := newLogrusBaseSuite(t)
25-
b.InterceptorTestSuite.ServerOpts = []grpc.ServerOption{
26-
grpc_middleware.WithStreamServerChain(
27-
grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
28-
grpc_logrus.StreamServerInterceptor(logrus.NewEntry(b.logger), opts...)),
29-
grpc_middleware.WithUnaryServerChain(
30-
grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
31-
grpc_logrus.UnaryServerInterceptor(logrus.NewEntry(b.logger), opts...)),
21+
for _, tcase := range []struct {
22+
timestampFormat string
23+
}{
24+
{
25+
timestampFormat: time.RFC3339,
26+
},
27+
{
28+
timestampFormat: "2006-01-02",
29+
},
30+
} {
31+
opts := []grpc_logrus.Option{
32+
grpc_logrus.WithLevels(customCodeToLevel),
33+
grpc_logrus.WithTimestampFormat(tcase.timestampFormat),
34+
}
35+
36+
b := newLogrusBaseSuite(t)
37+
b.timestampFormat = tcase.timestampFormat
38+
b.InterceptorTestSuite.ServerOpts = []grpc.ServerOption{
39+
grpc_middleware.WithStreamServerChain(
40+
grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
41+
grpc_logrus.StreamServerInterceptor(logrus.NewEntry(b.logger), opts...)),
42+
grpc_middleware.WithUnaryServerChain(
43+
grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
44+
grpc_logrus.UnaryServerInterceptor(logrus.NewEntry(b.logger), opts...)),
45+
}
46+
suite.Run(t, &logrusServerSuite{b})
3247
}
33-
suite.Run(t, &logrusServerSuite{b})
3448
}
3549

3650
type logrusServerSuite struct {
@@ -55,13 +69,13 @@ func (s *logrusServerSuite) TestPing_WithCustomTags() {
5569

5670
assert.Contains(s.T(), m, "custom_tags.int", "all lines must contain `custom_tags.int`")
5771
require.Contains(s.T(), m, "grpc.start_time", "all lines must contain the start time of the call")
58-
_, err := time.Parse(time.RFC3339, m["grpc.start_time"].(string))
59-
assert.NoError(s.T(), err, "should be able to parse start time as RFC3339")
72+
_, err := time.Parse(s.timestampFormat, m["grpc.start_time"].(string))
73+
assert.NoError(s.T(), err, "should be able to parse start time")
6074

6175
require.Contains(s.T(), m, "grpc.request.deadline", "all lines must contain the deadline of the call")
62-
_, err = time.Parse(time.RFC3339, m["grpc.request.deadline"].(string))
63-
require.NoError(s.T(), err, "should be able to parse deadline as RFC3339")
64-
assert.Equal(s.T(), m["grpc.request.deadline"], deadline.Format(time.RFC3339), "should have the same deadline that was set by the caller")
76+
_, err = time.Parse(s.timestampFormat, m["grpc.request.deadline"].(string))
77+
require.NoError(s.T(), err, "should be able to parse deadline")
78+
assert.Equal(s.T(), m["grpc.request.deadline"], deadline.Format(s.timestampFormat), "should have the same deadline that was set by the caller")
6579
}
6680

6781
assert.Equal(s.T(), msgs[0]["msg"], "some ping", "first message must contain the correct user message")
@@ -114,12 +128,12 @@ func (s *logrusServerSuite) TestPingError_WithCustomLevels() {
114128
assert.Equal(s.T(), m["msg"], "finished unary call with code "+tcase.code.String(), "must have the correct finish message")
115129

116130
require.Contains(s.T(), m, "grpc.start_time", "all lines must contain a start time for the call")
117-
_, err = time.Parse(time.RFC3339, m["grpc.start_time"].(string))
118-
assert.NoError(s.T(), err, "should be able to parse the start time as RFC3339")
131+
_, err = time.Parse(s.timestampFormat, m["grpc.start_time"].(string))
132+
assert.NoError(s.T(), err, "should be able to parse the start time")
119133

120134
require.Contains(s.T(), m, "grpc.request.deadline", "all lines must contain the deadline of the call")
121-
_, err = time.Parse(time.RFC3339, m["grpc.request.deadline"].(string))
122-
require.NoError(s.T(), err, "should be able to parse deadline as RFC3339")
135+
_, err = time.Parse(s.timestampFormat, m["grpc.request.deadline"].(string))
136+
require.NoError(s.T(), err, "should be able to parse deadline")
123137
}
124138
}
125139

@@ -145,12 +159,12 @@ func (s *logrusServerSuite) TestPingList_WithCustomTags() {
145159

146160
assert.Contains(s.T(), m, "custom_tags.int", "all lines must contain `custom_tags.int`")
147161
require.Contains(s.T(), m, "grpc.start_time", "all lines must contain the start time for the call")
148-
_, err := time.Parse(time.RFC3339, m["grpc.start_time"].(string))
162+
_, err := time.Parse(s.timestampFormat, m["grpc.start_time"].(string))
149163
assert.NoError(s.T(), err, "should be able to parse start time as RFC3339")
150164

151165
require.Contains(s.T(), m, "grpc.request.deadline", "all lines must contain the deadline of the call")
152-
_, err = time.Parse(time.RFC3339, m["grpc.request.deadline"].(string))
153-
require.NoError(s.T(), err, "should be able to parse deadline as RFC3339")
166+
_, err = time.Parse(s.timestampFormat, m["grpc.request.deadline"].(string))
167+
require.NoError(s.T(), err, "should be able to parse deadline")
154168
}
155169

156170
assert.Equal(s.T(), msgs[0]["msg"], "some pinglist", "msg must be the correct message")

logging/logrus/shared_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,10 @@ func (s *loggingPingService) PingEmpty(ctx context.Context, empty *pb_testproto.
5757

5858
type logrusBaseSuite struct {
5959
*grpc_testing.InterceptorTestSuite
60-
mutexBuffer *grpc_testing.MutexReadWriter
61-
buffer *bytes.Buffer
62-
logger *logrus.Logger
60+
mutexBuffer *grpc_testing.MutexReadWriter
61+
buffer *bytes.Buffer
62+
logger *logrus.Logger
63+
timestampFormat string
6364
}
6465

6566
func newLogrusBaseSuite(t *testing.T) *logrusBaseSuite {

0 commit comments

Comments
 (0)