Skip to content

Commit cb441f1

Browse files
committed
feat: add tracing to request validation
1 parent 7cf0595 commit cb441f1

File tree

8 files changed

+70
-56
lines changed

8 files changed

+70
-56
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
## [Unreleased]
77
### Changed
88
- Updated CI so that Postgres tests run against v18 which is GA and not against v13 which is EOL (https://github.com/authzed/spicedb/pull/2926)
9+
- Added tracing to request validation (https://github.com/authzed/spicedb/pull/2950)
910

1011
### Fixed
1112
- Regression introduced in 1.49.2: missing spans in ReadSchema calls (https://github.com/authzed/spicedb/pull/2947)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
// Package handwrittenvalidation defines middleware that runs custom-made validations on incoming requests.
1+
// Package handwrittenvalidation defines middleware that runs validations on incoming requests.
22
package handwrittenvalidation

internal/middleware/handwrittenvalidation/handwrittenvalidation.go

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,99 @@ package handwrittenvalidation
22

33
import (
44
"context"
5+
"fmt"
56

7+
"buf.build/go/protovalidate"
8+
"go.opentelemetry.io/otel"
9+
otelcodes "go.opentelemetry.io/otel/codes"
610
"google.golang.org/grpc"
711
"google.golang.org/grpc/codes"
812
"google.golang.org/grpc/status"
13+
"google.golang.org/protobuf/proto"
914
)
1015

16+
var tracer = otel.Tracer("spicedb/internal/middleware")
17+
18+
// mustNewProtoValidator wraps protovalidate.New() to panic
19+
// if the validator can't be constructed.
20+
func mustNewProtoValidator(opts ...protovalidate.ValidatorOption) protovalidate.Validator {
21+
validator, err := protovalidate.New(opts...)
22+
if err != nil {
23+
wrappedErr := fmt.Errorf("could not construct validator: %w", err)
24+
panic(wrappedErr)
25+
}
26+
return validator
27+
}
28+
1129
type handwrittenValidator interface {
1230
HandwrittenValidate() error
1331
}
1432

15-
// UnaryServerInterceptor returns a new unary server interceptor that runs the handwritten validation
16-
// on the incoming request, if any.
17-
func UnaryServerInterceptor(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
18-
validator, ok := req.(handwrittenValidator)
19-
if ok {
20-
err := validator.HandwrittenValidate()
33+
// UnaryServerInterceptor returns a function that performs proto and handwritten validation (if any) on the incoming request.
34+
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
35+
protovalidator := mustNewProtoValidator()
36+
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
37+
_, span := tracer.Start(ctx, "protovalidate")
38+
err := protovalidator.Validate(req.(proto.Message))
2139
if err != nil {
40+
span.SetStatus(otelcodes.Error, err.Error())
41+
span.RecordError(err)
42+
span.End()
2243
return nil, status.Errorf(codes.InvalidArgument, "%s", err)
2344
}
24-
}
2545

26-
return handler(ctx, req)
46+
validator, ok := req.(handwrittenValidator)
47+
if ok {
48+
_, span := tracer.Start(ctx, "handwritten.validation")
49+
err := validator.HandwrittenValidate()
50+
if err != nil {
51+
span.SetStatus(otelcodes.Error, err.Error())
52+
span.RecordError(err)
53+
span.End()
54+
return nil, status.Errorf(codes.InvalidArgument, "%s", err)
55+
}
56+
}
57+
58+
span.End()
59+
60+
return handler(ctx, req)
61+
}
2762
}
2863

29-
// StreamServerInterceptor returns a new stream server interceptor that runs the handwritten validation
30-
// on the incoming request messages, if any.
64+
// StreamServerInterceptor returns a function that performs proto and handwritten validation (if any) on the incoming request.
3165
func StreamServerInterceptor(srv any, stream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
32-
wrapper := &recvWrapper{stream}
66+
wrapper := &recvWrapper{ServerStream: stream, protovalidator: mustNewProtoValidator()}
3367
return handler(srv, wrapper)
3468
}
3569

3670
type recvWrapper struct {
3771
grpc.ServerStream
72+
protovalidator protovalidate.Validator
3873
}
3974

4075
func (s *recvWrapper) RecvMsg(m any) error {
4176
if err := s.ServerStream.RecvMsg(m); err != nil {
4277
return err
4378
}
4479

80+
_, span := tracer.Start(s.Context(), "protovalidate")
81+
82+
err := s.protovalidator.Validate(m.(proto.Message))
83+
if err != nil {
84+
span.SetStatus(otelcodes.Error, err.Error())
85+
span.RecordError(err)
86+
span.End()
87+
return status.Errorf(codes.InvalidArgument, "%s", err)
88+
}
89+
4590
validator, ok := m.(handwrittenValidator)
4691
if ok {
47-
err := validator.HandwrittenValidate()
92+
err = validator.HandwrittenValidate()
4893
if err != nil {
49-
return err
94+
span.SetStatus(otelcodes.Error, err.Error())
95+
span.RecordError(err)
96+
span.End()
97+
return status.Errorf(codes.InvalidArgument, "%s", err)
5098
}
5199
}
52100

internal/services/v1/experimental.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"time"
1414

1515
"github.com/ccoveille/go-safecast/v2"
16-
grpcvalidate "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate"
1716
"google.golang.org/grpc"
1817
"google.golang.org/grpc/codes"
1918
"google.golang.org/protobuf/types/known/timestamppb"
@@ -36,7 +35,6 @@ import (
3635
"github.com/authzed/spicedb/pkg/datastore"
3736
dsoptions "github.com/authzed/spicedb/pkg/datastore/options"
3837
"github.com/authzed/spicedb/pkg/datastore/queryshape"
39-
"github.com/authzed/spicedb/pkg/genutil"
4038
"github.com/authzed/spicedb/pkg/middleware/consistency"
4139
core "github.com/authzed/spicedb/pkg/proto/core/v1"
4240
dispatchv1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
@@ -94,18 +92,14 @@ func NewExperimentalServer(dispatch dispatch.Dispatcher, permServerConfig Permis
9492
chunkSize = 100
9593
}
9694

97-
validator := genutil.MustNewProtoValidator()
98-
9995
return &experimentalServer{
10096
WithServiceSpecificInterceptors: shared.WithServiceSpecificInterceptors{
10197
Unary: middleware.ChainUnaryServer(
102-
grpcvalidate.UnaryServerInterceptor(validator),
103-
handwrittenvalidation.UnaryServerInterceptor,
98+
handwrittenvalidation.UnaryServerInterceptor(),
10499
usagemetrics.UnaryServerInterceptor(),
105100
perfinsights.UnaryServerInterceptor(permServerConfig.PerformanceInsightMetricsEnabled),
106101
),
107102
Stream: middleware.ChainStreamServer(
108-
grpcvalidate.StreamServerInterceptor(validator),
109103
handwrittenvalidation.StreamServerInterceptor,
110104
usagemetrics.StreamServerInterceptor(),
111105
streamtimeout.MustStreamServerInterceptor(config.StreamReadTimeout),

internal/services/v1/relationships.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"fmt"
88
"time"
99

10-
grpcvalidate "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate"
1110
"github.com/prometheus/client_golang/prometheus"
1211
"github.com/prometheus/client_golang/prometheus/promauto"
1312
"go.opentelemetry.io/otel/trace"
@@ -146,20 +145,16 @@ func NewPermissionsServer(
146145
ExperimentalQueryPlan: config.ExperimentalQueryPlan,
147146
}
148147

149-
validator := genutil.MustNewProtoValidator()
150-
151148
return &permissionServer{
152149
dispatch: dispatch,
153150
config: configWithDefaults,
154151
WithServiceSpecificInterceptors: shared.WithServiceSpecificInterceptors{
155152
Unary: middleware.ChainUnaryServer(
156-
grpcvalidate.UnaryServerInterceptor(validator),
157-
handwrittenvalidation.UnaryServerInterceptor,
153+
handwrittenvalidation.UnaryServerInterceptor(),
158154
usagemetrics.UnaryServerInterceptor(),
159155
perfinsights.UnaryServerInterceptor(configWithDefaults.PerformanceInsightMetricsEnabled),
160156
),
161157
Stream: middleware.ChainStreamServer(
162-
grpcvalidate.StreamServerInterceptor(validator),
163158
handwrittenvalidation.StreamServerInterceptor,
164159
usagemetrics.StreamServerInterceptor(),
165160
streamtimeout.MustStreamServerInterceptor(configWithDefaults.StreamingAPITimeout),

internal/services/v1/schema.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import (
55
"sort"
66
"strings"
77

8-
grpcvalidate "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate"
9-
108
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
119

1210
log "github.com/authzed/spicedb/internal/logging"
1311
"github.com/authzed/spicedb/internal/middleware"
12+
"github.com/authzed/spicedb/internal/middleware/handwrittenvalidation"
1413
"github.com/authzed/spicedb/internal/middleware/perfinsights"
1514
"github.com/authzed/spicedb/internal/middleware/usagemetrics"
1615
"github.com/authzed/spicedb/internal/services/shared"
@@ -45,17 +44,15 @@ type SchemaServerConfig struct {
4544
func NewSchemaServer(config SchemaServerConfig) v1.SchemaServiceServer {
4645
cts := caveattypes.TypeSetOrDefault(config.CaveatTypeSet)
4746

48-
validator := genutil.MustNewProtoValidator()
49-
5047
return &schemaServer{
5148
WithServiceSpecificInterceptors: shared.WithServiceSpecificInterceptors{
5249
Unary: middleware.ChainUnaryServer(
53-
grpcvalidate.UnaryServerInterceptor(validator),
50+
handwrittenvalidation.UnaryServerInterceptor(),
5451
usagemetrics.UnaryServerInterceptor(),
5552
perfinsights.UnaryServerInterceptor(config.PerformanceInsightMetricsEnabled),
5653
),
5754
Stream: middleware.ChainStreamServer(
58-
grpcvalidate.StreamServerInterceptor(validator),
55+
handwrittenvalidation.StreamServerInterceptor,
5956
usagemetrics.StreamServerInterceptor(),
6057
perfinsights.StreamServerInterceptor(config.PerformanceInsightMetricsEnabled),
6158
),

internal/services/v1/watch.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@ import (
55
"slices"
66
"time"
77

8-
grpcvalidate "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate"
98
"google.golang.org/grpc/codes"
109
"google.golang.org/grpc/status"
1110
"google.golang.org/protobuf/types/known/structpb"
1211

1312
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
1413

14+
"github.com/authzed/spicedb/internal/middleware/handwrittenvalidation"
1515
"github.com/authzed/spicedb/internal/middleware/usagemetrics"
1616
"github.com/authzed/spicedb/internal/services/shared"
1717
"github.com/authzed/spicedb/pkg/datalayer"
1818
"github.com/authzed/spicedb/pkg/datastore"
19-
"github.com/authzed/spicedb/pkg/genutil"
2019
"github.com/authzed/spicedb/pkg/genutil/mapz"
2120
dispatchv1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
2221
"github.com/authzed/spicedb/pkg/tuple"
@@ -32,11 +31,9 @@ type watchServer struct {
3231

3332
// NewWatchServer creates an instance of the watch server.
3433
func NewWatchServer(heartbeatDuration time.Duration) v1.WatchServiceServer {
35-
validator := genutil.MustNewProtoValidator()
36-
3734
s := &watchServer{
3835
WithStreamServiceSpecificInterceptor: shared.WithStreamServiceSpecificInterceptor{
39-
Stream: grpcvalidate.StreamServerInterceptor(validator),
36+
Stream: handwrittenvalidation.StreamServerInterceptor,
4037
},
4138
heartbeatDuration: heartbeatDuration,
4239
}

pkg/genutil/protovalidate.go

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)