Skip to content

Commit 52a8392

Browse files
authored
gcp/observability: update method name validation (#5951)
1 parent 4075ef0 commit 52a8392

File tree

3 files changed

+159
-23
lines changed

3 files changed

+159
-23
lines changed

gcp/observability/config.go

+23-10
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,14 @@ import (
2424
"errors"
2525
"fmt"
2626
"os"
27-
"regexp"
27+
"strings"
2828

2929
gcplogging "cloud.google.com/go/logging"
3030
"golang.org/x/oauth2/google"
3131
"google.golang.org/grpc/internal/envconfig"
3232
)
3333

34-
const (
35-
envProjectID = "GOOGLE_CLOUD_PROJECT"
36-
methodStringRegexpStr = `^([\w./]+)/((?:\w+)|[*])$`
37-
)
38-
39-
var methodStringRegexp = regexp.MustCompile(methodStringRegexpStr)
34+
const envProjectID = "GOOGLE_CLOUD_PROJECT"
4035

4136
// fetchDefaultProjectID fetches the default GCP project id from environment.
4237
func fetchDefaultProjectID(ctx context.Context) string {
@@ -59,6 +54,25 @@ func fetchDefaultProjectID(ctx context.Context) string {
5954
return credentials.ProjectID
6055
}
6156

57+
// validateMethodString validates whether the string passed in is a valid
58+
// pattern.
59+
func validateMethodString(method string) error {
60+
if strings.HasPrefix(method, "/") {
61+
return errors.New("cannot have a leading slash")
62+
}
63+
serviceMethod := strings.Split(method, "/")
64+
if len(serviceMethod) != 2 {
65+
return errors.New("/ must come in between service and method, only one /")
66+
}
67+
if serviceMethod[1] == "" {
68+
return errors.New("method name must be non empty")
69+
}
70+
if serviceMethod[0] == "*" {
71+
return errors.New("cannot have service wildcard * i.e. (*/m)")
72+
}
73+
return nil
74+
}
75+
6276
func validateLogEventMethod(methods []string, exclude bool) error {
6377
for _, method := range methods {
6478
if method == "*" {
@@ -67,9 +81,8 @@ func validateLogEventMethod(methods []string, exclude bool) error {
6781
}
6882
continue
6983
}
70-
match := methodStringRegexp.FindStringSubmatch(method)
71-
if match == nil {
72-
return fmt.Errorf("invalid method string: %v", method)
84+
if err := validateMethodString(method); err != nil {
85+
return fmt.Errorf("invalid method string: %v, err: %v", method, err)
7386
}
7487
}
7588
return nil

gcp/observability/logging.go

+18-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"bytes"
2323
"context"
2424
"encoding/base64"
25+
"errors"
2526
"fmt"
2627
"strings"
2728
"time"
@@ -322,6 +323,7 @@ func (bml *binaryMethodLogger) Log(c iblog.LogEntryConfig) {
322323
}
323324

324325
type eventConfig struct {
326+
// ServiceMethod has /s/m syntax for fast matching.
325327
ServiceMethod map[string]bool
326328
Services map[string]bool
327329
MatchAll bool
@@ -364,6 +366,17 @@ func (bl *binaryLogger) GetMethodLogger(methodName string) iblog.MethodLogger {
364366
return nil
365367
}
366368

369+
// parseMethod splits service and method from the input. It expects format
370+
// "service/method".
371+
func parseMethod(method string) (string, string, error) {
372+
pos := strings.Index(method, "/")
373+
if pos < 0 {
374+
// Shouldn't happen, config already validated.
375+
return "", "", errors.New("invalid method name: no / found")
376+
}
377+
return method[:pos], method[pos+1:], nil
378+
}
379+
367380
func registerClientRPCEvents(clientRPCEvents []clientRPCEvents, exporter loggingExporter) {
368381
if len(clientRPCEvents) == 0 {
369382
return
@@ -382,15 +395,15 @@ func registerClientRPCEvents(clientRPCEvents []clientRPCEvents, exporter logging
382395
eventConfig.MatchAll = true
383396
continue
384397
}
385-
s, m, err := grpcutil.ParseMethod(method)
398+
s, m, err := parseMethod(method)
386399
if err != nil {
387400
continue
388401
}
389402
if m == "*" {
390403
eventConfig.Services[s] = true
391404
continue
392405
}
393-
eventConfig.ServiceMethod[method] = true
406+
eventConfig.ServiceMethod["/"+method] = true
394407
}
395408
eventConfigs = append(eventConfigs, eventConfig)
396409
}
@@ -419,15 +432,15 @@ func registerServerRPCEvents(serverRPCEvents []serverRPCEvents, exporter logging
419432
eventConfig.MatchAll = true
420433
continue
421434
}
422-
s, m, err := grpcutil.ParseMethod(method)
423-
if err != nil { // Shouldn't happen, already validated at this point.
435+
s, m, err := parseMethod(method)
436+
if err != nil {
424437
continue
425438
}
426439
if m == "*" {
427440
eventConfig.Services[s] = true
428441
continue
429442
}
430-
eventConfig.ServiceMethod[method] = true
443+
eventConfig.ServiceMethod["/"+method] = true
431444
}
432445
eventConfigs = append(eventConfigs, eventConfig)
433446
}

gcp/observability/logging_test.go

+118-8
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"encoding/json"
2525
"fmt"
2626
"io"
27+
"strings"
2728
"sync"
2829
"testing"
2930

@@ -99,13 +100,14 @@ func setupObservabilitySystemWithConfig(cfg *config) (func(), error) {
99100
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
100101
defer cancel()
101102
err = Start(ctx)
102-
if err != nil {
103-
return nil, fmt.Errorf("error in Start: %v", err)
104-
}
105-
return func() {
103+
cleanup := func() {
106104
End()
107105
envconfig.ObservabilityConfig = oldObservabilityConfig
108-
}, nil
106+
}
107+
if err != nil {
108+
return cleanup, fmt.Errorf("error in Start: %v", err)
109+
}
110+
return cleanup, nil
109111
}
110112

111113
// TestClientRPCEventsLogAll tests the observability system configured with a
@@ -777,18 +779,18 @@ func (s) TestPrecedenceOrderingInConfiguration(t *testing.T) {
777779
CloudLogging: &cloudLogging{
778780
ClientRPCEvents: []clientRPCEvents{
779781
{
780-
Methods: []string{"/grpc.testing.TestService/UnaryCall"},
782+
Methods: []string{"grpc.testing.TestService/UnaryCall"},
781783
MaxMetadataBytes: 30,
782784
MaxMessageBytes: 30,
783785
},
784786
{
785-
Methods: []string{"/grpc.testing.TestService/EmptyCall"},
787+
Methods: []string{"grpc.testing.TestService/EmptyCall"},
786788
Exclude: true,
787789
MaxMetadataBytes: 30,
788790
MaxMessageBytes: 30,
789791
},
790792
{
791-
Methods: []string{"/grpc.testing.TestService/*"},
793+
Methods: []string{"grpc.testing.TestService/*"},
792794
MaxMetadataBytes: 30,
793795
MaxMessageBytes: 30,
794796
},
@@ -1273,3 +1275,111 @@ func (s) TestMetadataTruncationAccountsKey(t *testing.T) {
12731275
}
12741276
fle.mu.Unlock()
12751277
}
1278+
1279+
// TestMethodInConfiguration tests different method names with an expectation on
1280+
// whether they should error or not.
1281+
func (s) TestMethodInConfiguration(t *testing.T) {
1282+
// To skip creating a stackdriver exporter.
1283+
fle := &fakeLoggingExporter{
1284+
t: t,
1285+
}
1286+
1287+
defer func(ne func(ctx context.Context, config *config) (loggingExporter, error)) {
1288+
newLoggingExporter = ne
1289+
}(newLoggingExporter)
1290+
1291+
newLoggingExporter = func(ctx context.Context, config *config) (loggingExporter, error) {
1292+
return fle, nil
1293+
}
1294+
1295+
tests := []struct {
1296+
name string
1297+
config *config
1298+
wantErr string
1299+
}{
1300+
{
1301+
name: "leading-slash",
1302+
config: &config{
1303+
ProjectID: "fake",
1304+
CloudLogging: &cloudLogging{
1305+
ClientRPCEvents: []clientRPCEvents{
1306+
{
1307+
Methods: []string{"/service/method"},
1308+
},
1309+
},
1310+
},
1311+
},
1312+
wantErr: "cannot have a leading slash",
1313+
},
1314+
{
1315+
name: "wildcard service/method",
1316+
config: &config{
1317+
ProjectID: "fake",
1318+
CloudLogging: &cloudLogging{
1319+
ClientRPCEvents: []clientRPCEvents{
1320+
{
1321+
Methods: []string{"*/method"},
1322+
},
1323+
},
1324+
},
1325+
},
1326+
wantErr: "cannot have service wildcard *",
1327+
},
1328+
{
1329+
name: "/ in service name",
1330+
config: &config{
1331+
ProjectID: "fake",
1332+
CloudLogging: &cloudLogging{
1333+
ClientRPCEvents: []clientRPCEvents{
1334+
{
1335+
Methods: []string{"ser/vice/method"},
1336+
},
1337+
},
1338+
},
1339+
},
1340+
wantErr: "only one /",
1341+
},
1342+
{
1343+
name: "empty method name",
1344+
config: &config{
1345+
ProjectID: "fake",
1346+
CloudLogging: &cloudLogging{
1347+
ClientRPCEvents: []clientRPCEvents{
1348+
{
1349+
Methods: []string{"service/"},
1350+
},
1351+
},
1352+
},
1353+
},
1354+
wantErr: "method name must be non empty",
1355+
},
1356+
{
1357+
name: "normal",
1358+
config: &config{
1359+
ProjectID: "fake",
1360+
CloudLogging: &cloudLogging{
1361+
ClientRPCEvents: []clientRPCEvents{
1362+
{
1363+
Methods: []string{"service/method"},
1364+
},
1365+
},
1366+
},
1367+
},
1368+
wantErr: "",
1369+
},
1370+
}
1371+
for _, test := range tests {
1372+
t.Run(test.name, func(t *testing.T) {
1373+
cleanup, gotErr := setupObservabilitySystemWithConfig(test.config)
1374+
if cleanup != nil {
1375+
defer cleanup()
1376+
}
1377+
if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) {
1378+
t.Fatalf("Start(%v) = %v, wantErr %v", test.config, gotErr, test.wantErr)
1379+
}
1380+
if (gotErr != nil) != (test.wantErr != "") {
1381+
t.Fatalf("Start(%v) = %v, wantErr %v", test.config, gotErr, test.wantErr)
1382+
}
1383+
})
1384+
}
1385+
}

0 commit comments

Comments
 (0)