Skip to content

Commit a78cb17

Browse files
logging: add WithErrorFields (#734)
WithErrorFields allows to extract logging fields from an error. Typically to add a stack trace field or more context around the error. Signed-off-by: Olivier Cano <[email protected]>
1 parent 6aea589 commit a78cb17

File tree

5 files changed

+64
-3
lines changed

5 files changed

+64
-3
lines changed

interceptors/logging/interceptors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ func (c *reporter) PostCall(err error, duration time.Duration) {
4040
fields = fields.AppendUnique(Fields{"grpc.code", code.String()})
4141
if err != nil {
4242
fields = fields.AppendUnique(Fields{"grpc.error", fmt.Sprintf("%v", err)})
43+
if c.opts.errorToFieldsFunc != nil {
44+
fields = fields.AppendUnique(c.opts.errorToFieldsFunc(err))
45+
}
4346
}
4447
if c.opts.fieldsFromCtxCallMetaFn != nil {
4548
// fieldsFromCtxFn dups override the existing fields.
@@ -53,6 +56,9 @@ func (c *reporter) PostMsgSend(payload any, err error, duration time.Duration) {
5356
fields := c.fields.WithUnique(ExtractFields(c.ctx))
5457
if err != nil {
5558
fields = fields.AppendUnique(Fields{"grpc.error", fmt.Sprintf("%v", err)})
59+
if c.opts.errorToFieldsFunc != nil {
60+
fields = fields.AppendUnique(c.opts.errorToFieldsFunc(err))
61+
}
5662
}
5763
if c.opts.fieldsFromCtxCallMetaFn != nil {
5864
// fieldsFromCtxFn dups override the existing fields.
@@ -104,6 +110,9 @@ func (c *reporter) PostMsgReceive(payload any, err error, duration time.Duration
104110
fields := c.fields.WithUnique(ExtractFields(c.ctx))
105111
if err != nil {
106112
fields = fields.AppendUnique(Fields{"grpc.error", fmt.Sprintf("%v", err)})
113+
if c.opts.errorToFieldsFunc != nil {
114+
fields = fields.AppendUnique(c.opts.errorToFieldsFunc(err))
115+
}
107116
}
108117
if c.opts.fieldsFromCtxCallMetaFn != nil {
109118
// fieldsFromCtxFn dups override the existing fields.

interceptors/logging/interceptors_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,12 @@ func TestSuite(t *testing.T) {
205205
logging.StreamClientInterceptor(s.logger, logging.WithLevels(customClientCodeToLevel), logging.WithFieldsFromContext(customFields)),
206206
),
207207
}
208+
errorFields := func(err error) logging.Fields {
209+
return testpb.ExtractErrorFields(err)
210+
}
208211
s.InterceptorTestSuite.ServerOpts = []grpc.ServerOption{
209-
grpc.StreamInterceptor(logging.StreamServerInterceptor(s.logger, logging.WithLevels(customClientCodeToLevel), logging.WithFieldsFromContext(customFields))),
210-
grpc.UnaryInterceptor(logging.UnaryServerInterceptor(s.logger, logging.WithLevels(customClientCodeToLevel), logging.WithFieldsFromContext(customFields))),
212+
grpc.StreamInterceptor(logging.StreamServerInterceptor(s.logger, logging.WithLevels(customClientCodeToLevel), logging.WithFieldsFromContext(customFields), logging.WithErrorFields(errorFields))),
213+
grpc.UnaryInterceptor(logging.UnaryServerInterceptor(s.logger, logging.WithLevels(customClientCodeToLevel), logging.WithFieldsFromContext(customFields), logging.WithErrorFields(errorFields))),
211214
}
212215
suite.Run(t, s)
213216
}
@@ -367,6 +370,7 @@ func (s *loggingClientServerSuite) TestPingError_WithCustomLevels() {
367370
AssertFieldNotEmpty(t, "grpc.request.deadline").
368371
AssertField(t, "grpc.code", tcase.code.String()).
369372
AssertField(t, "grpc.error", fmt.Sprintf("rpc error: code = %s desc = Userspace error", tcase.code.String())).
373+
AssertField(t, "error-field", "plop").
370374
AssertFieldNotEmpty(s.T(), "grpc.time_ms").AssertNoMoreTags(s.T())
371375

372376
clientFinishCallLogLine := lines[0]

interceptors/logging/options.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ var (
5757
type options struct {
5858
levelFunc CodeToLevel
5959
loggableEvents []LoggableEvent
60+
errorToFieldsFunc ErrorToFields
6061
codeFunc ErrorToCode
6162
durationFieldFunc DurationToFields
6263
timestampFormat string
@@ -89,6 +90,9 @@ func evaluateClientOpt(opts []Option) *options {
8990
// DurationToFields function defines how to produce duration fields for logging.
9091
type DurationToFields func(duration time.Duration) Fields
9192

93+
// ErrorToFields function extract fields from error.
94+
type ErrorToFields func(err error) Fields
95+
9296
// ErrorToCode function determines the error code of an error.
9397
// This makes using custom errors with grpc middleware easier.
9498
type ErrorToCode func(err error) codes.Code
@@ -169,6 +173,13 @@ func WithLogOnEvents(events ...LoggableEvent) Option {
169173
}
170174
}
171175

176+
// WithErrorFields allows to extract logging fields from an error.
177+
func WithErrorFields(f ErrorToFields) Option {
178+
return func(o *options) {
179+
o.errorToFieldsFunc = f
180+
}
181+
}
182+
172183
// WithLevels customizes the function for mapping gRPC return codes and interceptor log level statements.
173184
func WithLevels(f CodeToLevel) Option {
174185
return func(o *options) {

testing/testpb/interceptor_suite.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"crypto/x509"
1212
"crypto/x509/pkix"
1313
"encoding/pem"
14+
"errors"
1415
"flag"
1516
"math/big"
1617
"net"
@@ -20,8 +21,10 @@ import (
2021
"github.com/stretchr/testify/require"
2122
"github.com/stretchr/testify/suite"
2223
"google.golang.org/grpc"
24+
"google.golang.org/grpc/codes"
2325
"google.golang.org/grpc/credentials"
2426
"google.golang.org/grpc/credentials/insecure"
27+
"google.golang.org/grpc/status"
2528
)
2629

2730
var (
@@ -150,6 +153,40 @@ func ExtractCtxTestNumber(ctx context.Context) *int {
150153
return &zero
151154
}
152155

156+
type wrappedErrFields struct {
157+
wrappedErr error
158+
fields []any
159+
}
160+
161+
func (err *wrappedErrFields) Unwrap() error {
162+
return err.wrappedErr
163+
}
164+
165+
func (err *wrappedErrFields) Error() string {
166+
// Ideally we print wrapped fields as well
167+
return err.wrappedErr.Error()
168+
}
169+
170+
func (err *wrappedErrFields) GRPCStatus() *status.Status {
171+
if s, ok := status.FromError(err.wrappedErr); ok {
172+
return s
173+
}
174+
return status.New(codes.Unknown, err.Error())
175+
}
176+
177+
func WrapFieldsInError(err error, fields []any) error {
178+
return &wrappedErrFields{err, fields}
179+
}
180+
181+
func ExtractErrorFields(err error) []any {
182+
var e *wrappedErrFields
183+
ok := errors.As(err, &e)
184+
if !ok {
185+
return nil
186+
}
187+
return e.fields
188+
}
189+
153190
// UnaryServerInterceptor returns a new unary server interceptors that adds query information logging.
154191
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
155192
return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {

testing/testpb/pingservice.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (s *TestPingService) Ping(ctx context.Context, ping *PingRequest) (*PingRes
4545

4646
func (s *TestPingService) PingError(_ context.Context, ping *PingErrorRequest) (*PingErrorResponse, error) {
4747
code := codes.Code(ping.ErrorCodeReturned)
48-
return nil, status.Error(code, "Userspace error")
48+
return nil, WrapFieldsInError(status.Error(code, "Userspace error"), []any{"error-field", "plop"})
4949
}
5050

5151
func (s *TestPingService) PingList(ping *PingListRequest, stream TestService_PingListServer) error {

0 commit comments

Comments
 (0)