diff --git a/internal/binarylog/binarylog.go b/internal/binarylog/binarylog.go new file mode 100644 index 00000000..4c80e7c1 --- /dev/null +++ b/internal/binarylog/binarylog.go @@ -0,0 +1,141 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package binarylog implementation binary logging as defined in +// https://github.com/grpc/proposal/blob/master/A16-binary-logging.md. +package binarylog + +import ( + "fmt" + "os" + + "google.golang.org/grpc/grpclog" +) + +// Logger is the global binary logger for the binary. One of this should be +// built at init time from the configuration (environment varialbe or flags). +// +// It is used to get a methodLogger for each individual method. +var Logger *logger + +func init() { + const envStr = "GRPC_BINARY_LOG_FILTER" + configStr := os.Getenv(envStr) + Logger = newLoggerFromConfigString(configStr) +} + +type methodLoggerConfig struct { + // Max length of header and message. + hdr, msg uint64 +} + +type logger struct { + all *methodLoggerConfig + services map[string]*methodLoggerConfig + methods map[string]*methodLoggerConfig + + blacklist map[string]struct{} +} + +// newEmptyLogger creates an empty logger. The map fields need to be filled in +// using the set* functions. +func newEmptyLogger() *logger { + return &logger{} +} + +// Set method logger for "*". +func (l *logger) setDefaultMethodLogger(ml *methodLoggerConfig) error { + if l.all != nil { + return fmt.Errorf("conflicting global rules found") + } + l.all = ml + return nil +} + +// Set method logger for "service/*". +// +// New methodLogger with same service overrides the old one. +func (l *logger) setServiceMethodLogger(service string, ml *methodLoggerConfig) error { + if _, ok := l.services[service]; ok { + return fmt.Errorf("conflicting rules for service %v found", service) + } + if l.services == nil { + l.services = make(map[string]*methodLoggerConfig) + } + l.services[service] = ml + return nil +} + +// Set method logger for "service/method". +// +// New methodLogger with same method overrides the old one. +func (l *logger) setMethodMethodLogger(method string, ml *methodLoggerConfig) error { + if _, ok := l.blacklist[method]; ok { + return fmt.Errorf("conflicting rules for method %v found", method) + } + if _, ok := l.methods[method]; ok { + return fmt.Errorf("conflicting rules for method %v found", method) + } + if l.methods == nil { + l.methods = make(map[string]*methodLoggerConfig) + } + l.methods[method] = ml + return nil +} + +// Set blacklist method for "-service/method". +func (l *logger) setBlacklist(method string) error { + if _, ok := l.blacklist[method]; ok { + return fmt.Errorf("conflicting rules for method %v found", method) + } + if _, ok := l.methods[method]; ok { + return fmt.Errorf("conflicting rules for method %v found", method) + } + if l.blacklist == nil { + l.blacklist = make(map[string]struct{}) + } + l.blacklist[method] = struct{}{} + return nil +} + +// GetMethodLogger returns the methodLogger for the given methodName. +// +// methodName should be in the format of "/service/method". +// +// Each methodLogger returned by this method is a new instance. This is to +// generate sequence id within the call. +func (l *logger) GetMethodLogger(methodName string) *MethodLogger { + s, m, err := parseMethodName(methodName) + if err != nil { + grpclog.Infof("binarylogging: failed to parse %q: %v", methodName, err) + return nil + } + if ml, ok := l.methods[s+"/"+m]; ok { + return newMethodLogger(ml.hdr, ml.msg) + } + if _, ok := l.blacklist[s+"/"+m]; ok { + return nil + } + if ml, ok := l.services[s]; ok { + return newMethodLogger(ml.hdr, ml.msg) + } + if l.all == nil { + return nil + } + return newMethodLogger(l.all.hdr, l.all.msg) +} diff --git a/internal/binarylog/binarylog_test.go b/internal/binarylog/binarylog_test.go new file mode 100644 index 00000000..acfbbd44 --- /dev/null +++ b/internal/binarylog/binarylog_test.go @@ -0,0 +1,147 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package binarylog + +import ( + "testing" +) + +// Test that get method logger returns the one with the most exact match. +func TestGetMethodLogger(t *testing.T) { + testCases := []struct { + in string + method string + hdr, msg uint64 + }{ + // Global. + { + in: "*{h:12;m:23}", + method: "/s/m", + hdr: 12, msg: 23, + }, + // service/*. + { + in: "*,s/*{h:12;m:23}", + method: "/s/m", + hdr: 12, msg: 23, + }, + // Service/method. + { + in: "*{h;m},s/m{h:12;m:23}", + method: "/s/m", + hdr: 12, msg: 23, + }, + { + in: "*{h;m},s/*{h:314;m},s/m{h:12;m:23}", + method: "/s/m", + hdr: 12, msg: 23, + }, + { + in: "*{h;m},s/*{h:12;m:23},s/m", + method: "/s/m", + hdr: maxUInt, msg: maxUInt, + }, + + // service/*. + { + in: "*{h;m},s/*{h:12;m:23},s/m1", + method: "/s/m", + hdr: 12, msg: 23, + }, + { + in: "*{h;m},s1/*,s/m{h:12;m:23}", + method: "/s/m", + hdr: 12, msg: 23, + }, + + // With black list. + { + in: "*{h:12;m:23},-s/m1", + method: "/s/m", + hdr: 12, msg: 23, + }, + } + for _, tc := range testCases { + l := newLoggerFromConfigString(tc.in) + if l == nil { + t.Errorf("in: %q, failed to create logger from config string", tc.in) + continue + } + ml := l.GetMethodLogger(tc.method) + if ml == nil { + t.Errorf("in: %q, method logger is nil, want non-nil", tc.in) + continue + } + + if ml.headerMaxLen != tc.hdr || ml.messageMaxLen != tc.msg { + t.Errorf("in: %q, want header: %v, message: %v, got header: %v, message: %v", tc.in, tc.hdr, tc.msg, ml.headerMaxLen, ml.messageMaxLen) + } + } +} + +// expect method logger to be nil +func TestGetMethodLoggerOff(t *testing.T) { + testCases := []struct { + in string + method string + }{ + // method not specified. + { + in: "s1/m", + method: "/s/m", + }, + { + in: "s/m1", + method: "/s/m", + }, + { + in: "s1/*", + method: "/s/m", + }, + { + in: "s1/*,s/m1", + method: "/s/m", + }, + + // blacklisted. + { + in: "*,-s/m", + method: "/s/m", + }, + { + in: "s/*,-s/m", + method: "/s/m", + }, + { + in: "-s/m,s/*", + method: "/s/m", + }, + } + for _, tc := range testCases { + l := newLoggerFromConfigString(tc.in) + if l == nil { + t.Errorf("in: %q, failed to create logger from config string", tc.in) + continue + } + ml := l.GetMethodLogger(tc.method) + if ml != nil { + t.Errorf("in: %q, method logger is non-nil, want nil", tc.in) + } + } +} diff --git a/internal/binarylog/env_config.go b/internal/binarylog/env_config.go new file mode 100644 index 00000000..ad234706 --- /dev/null +++ b/internal/binarylog/env_config.go @@ -0,0 +1,206 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package binarylog + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" + + "google.golang.org/grpc/grpclog" +) + +// newLoggerFromConfigString reads the string and build a logger. +// +// Example filter config strings: +// - "" Nothing will be logged +// - "*" All headers and messages will be fully logged. +// - "*{h}" Only headers will be logged. +// - "*{m:256}" Only the first 256 bytes of each message will be logged. +// - "Foo/*" Logs every method in service Foo +// - "Foo/*,-Foo/Bar" Logs every method in service Foo except method /Foo/Bar +// - "Foo/*,Foo/Bar{m:256}" Logs the first 256 bytes of each message in method +// /Foo/Bar, logs all headers and messages in every other method in service +// Foo. +// +// If two configs exist for one certain method or service, the one specified +// later overrides the privous config. +func newLoggerFromConfigString(s string) *logger { + l := newEmptyLogger() + methods := strings.Split(s, ",") + for _, method := range methods { + if err := l.fillMethodLoggerWithConfigString(method); err != nil { + grpclog.Warningf("failed to parse binary log config: %v", err) + return nil + } + } + return l +} + +// fillMethodLoggerWithConfigString parses config, creates methodLogger and adds +// it to the right map in the logger. +func (l *logger) fillMethodLoggerWithConfigString(config string) error { + // "" is invalid. + if config == "" { + return errors.New("empty string is not a valid method binary logging config") + } + + // "-service/method", blacklist, no * or {} allowed. + if config[0] == '-' { + s, m, suffix, err := parseMethodConfigAndSuffix(config[1:]) + if err != nil { + return fmt.Errorf("invalid config: %q, %v", config, err) + } + if m == "*" { + return fmt.Errorf("invalid config: %q, %v", config, "* not allowd in blacklist config") + } + if suffix != "" { + return fmt.Errorf("invalid config: %q, %v", config, "header/message limit not allowed in blacklist config") + } + if err := l.setBlacklist(s + "/" + m); err != nil { + return fmt.Errorf("invalid config: %v", err) + } + return nil + } + + // "*{h:256;m:256}" + if config[0] == '*' { + hdr, msg, err := parseHeaderMessageLengthConfig(config[1:]) + if err != nil { + return fmt.Errorf("invalid config: %q, %v", config, err) + } + if err := l.setDefaultMethodLogger(&methodLoggerConfig{hdr: hdr, msg: msg}); err != nil { + return fmt.Errorf("invalid config: %v", err) + } + return nil + } + + s, m, suffix, err := parseMethodConfigAndSuffix(config) + if err != nil { + return fmt.Errorf("invalid config: %q, %v", config, err) + } + hdr, msg, err := parseHeaderMessageLengthConfig(suffix) + if err != nil { + return fmt.Errorf("invalid header/message length config: %q, %v", suffix, err) + } + if m == "*" { + if err := l.setServiceMethodLogger(s, &methodLoggerConfig{hdr: hdr, msg: msg}); err != nil { + return fmt.Errorf("invalid config: %v", err) + } + } else { + if err := l.setMethodMethodLogger(s+"/"+m, &methodLoggerConfig{hdr: hdr, msg: msg}); err != nil { + return fmt.Errorf("invalid config: %v", err) + } + } + return nil +} + +const ( + // TODO: this const is only used by env_config now. But could be useful for + // other config. Move to binarylog.go if necessary. + maxUInt = ^uint64(0) + + // For "p.s/m" plus any suffix. Suffix will be parsed again. See test for + // expected output. + longMethodConfigRegexpStr = `^([\w./]+)/((?:\w+)|[*])(.+)?$` + + // For suffix from above, "{h:123,m:123}". See test for expected output. + optionalLengthRegexpStr = `(?::(\d+))?` // Optional ":123". + headerConfigRegexpStr = `^{h` + optionalLengthRegexpStr + `}$` + messageConfigRegexpStr = `^{m` + optionalLengthRegexpStr + `}$` + headerMessageConfigRegexpStr = `^{h` + optionalLengthRegexpStr + `;m` + optionalLengthRegexpStr + `}$` +) + +var ( + longMethodConfigRegexp = regexp.MustCompile(longMethodConfigRegexpStr) + headerConfigRegexp = regexp.MustCompile(headerConfigRegexpStr) + messageConfigRegexp = regexp.MustCompile(messageConfigRegexpStr) + headerMessageConfigRegexp = regexp.MustCompile(headerMessageConfigRegexpStr) +) + +// Turn "service/method{h;m}" into "service", "method", "{h;m}". +func parseMethodConfigAndSuffix(c string) (service, method, suffix string, _ error) { + // Regexp result: + // + // in: "p.s/m{h:123,m:123}", + // out: []string{"p.s/m{h:123,m:123}", "p.s", "m", "{h:123,m:123}"}, + match := longMethodConfigRegexp.FindStringSubmatch(c) + if match == nil { + return "", "", "", fmt.Errorf("%q contains invalid substring", c) + } + service = match[1] + method = match[2] + suffix = match[3] + return +} + +// Turn "{h:123;m:345}" into 123, 345. +// +// Return maxUInt if length is unspecified. +func parseHeaderMessageLengthConfig(c string) (hdrLenStr, msgLenStr uint64, err error) { + if c == "" { + return maxUInt, maxUInt, nil + } + // Header config only. + if match := headerConfigRegexp.FindStringSubmatch(c); match != nil { + if s := match[1]; s != "" { + hdrLenStr, err = strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("failed to convert %q to uint", s) + } + return hdrLenStr, 0, nil + } + return maxUInt, 0, nil + } + + // Message config only. + if match := messageConfigRegexp.FindStringSubmatch(c); match != nil { + if s := match[1]; s != "" { + msgLenStr, err = strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("Failed to convert %q to uint", s) + } + return 0, msgLenStr, nil + } + return 0, maxUInt, nil + } + + // Header and message config both. + if match := headerMessageConfigRegexp.FindStringSubmatch(c); match != nil { + // Both hdr and msg are specified, but one or two of them might be empty. + hdrLenStr = maxUInt + msgLenStr = maxUInt + if s := match[1]; s != "" { + hdrLenStr, err = strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("Failed to convert %q to uint", s) + } + } + if s := match[2]; s != "" { + msgLenStr, err = strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("Failed to convert %q to uint", s) + } + } + return hdrLenStr, msgLenStr, nil + } + return 0, 0, fmt.Errorf("%q contains invalid substring", c) +} diff --git a/internal/binarylog/env_config_test.go b/internal/binarylog/env_config_test.go new file mode 100644 index 00000000..44680162 --- /dev/null +++ b/internal/binarylog/env_config_test.go @@ -0,0 +1,478 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package binarylog + +import ( + "fmt" + "testing" +) + +// This tests that when multiple configs are specified, all methods loggers will +// be set correctly. Correctness of each logger is covered by other unit tests. +func TestNewLoggerFromConfigString(t *testing.T) { + const ( + s1 = "s1" + m1 = "m1" + m2 = "m2" + fullM1 = s1 + "/" + m1 + fullM2 = s1 + "/" + m2 + ) + c := fmt.Sprintf("*{h:1;m:2},%s{h},%s{m},%s{h;m}", s1+"/*", fullM1, fullM2) + l := newLoggerFromConfigString(c) + + if l.all.hdr != 1 || l.all.msg != 2 { + t.Errorf("l.all = %#v, want headerLen: 1, messageLen: 2", l.all) + } + + if ml, ok := l.services[s1]; ok { + if ml.hdr != maxUInt || ml.msg != 0 { + t.Errorf("want maxUInt header, 0 message, got header: %v, message: %v", ml.hdr, ml.msg) + } + } else { + t.Errorf("service/* is not set") + } + + if ml, ok := l.methods[fullM1]; ok { + if ml.hdr != 0 || ml.msg != maxUInt { + t.Errorf("want 0 header, maxUInt message, got header: %v, message: %v", ml.hdr, ml.msg) + } + } else { + t.Errorf("service/method{h} is not set") + } + + if ml, ok := l.methods[fullM2]; ok { + if ml.hdr != maxUInt || ml.msg != maxUInt { + t.Errorf("want maxUInt header, maxUInt message, got header: %v, message: %v", ml.hdr, ml.msg) + } + } else { + t.Errorf("service/method{h;m} is not set") + } +} + +func TestNewLoggerFromConfigStringInvalid(t *testing.T) { + testCases := []string{ + "", + "*{}", + "s/m,*{}", + "s/m,s/m{a}", + + // Duplciate rules. + "s/m,-s/m", + "-s/m,s/m", + "s/m,s/m", + "s/m,s/m{h:1;m:1}", + "s/m{h:1;m:1},s/m", + "-s/m,-s/m", + "s/*,s/*{h:1;m:1}", + "*,*{h:1;m:1}", + } + for _, tc := range testCases { + l := newLoggerFromConfigString(tc) + if l != nil { + t.Errorf("With config %q, want logger %v, got %v", tc, nil, l) + } + } +} + +func TestParseMethodConfigAndSuffix(t *testing.T) { + testCases := []struct { + in, service, method, suffix string + }{ + { + in: "p.s/m", + service: "p.s", method: "m", suffix: "", + }, + { + in: "p.s/m{h,m}", + service: "p.s", method: "m", suffix: "{h,m}", + }, + { + in: "p.s/*", + service: "p.s", method: "*", suffix: "", + }, + { + in: "p.s/*{h,m}", + service: "p.s", method: "*", suffix: "{h,m}", + }, + + // invalid suffix will be detected by another function. + { + in: "p.s/m{invalidsuffix}", + service: "p.s", method: "m", suffix: "{invalidsuffix}", + }, + { + in: "p.s/*{invalidsuffix}", + service: "p.s", method: "*", suffix: "{invalidsuffix}", + }, + { + in: "s/m*", + service: "s", method: "m", suffix: "*", + }, + { + in: "s/*m", + service: "s", method: "*", suffix: "m", + }, + { + in: "s/**", + service: "s", method: "*", suffix: "*", + }, + } + for _, tc := range testCases { + t.Logf("testing parseMethodConfigAndSuffix(%q)", tc.in) + s, m, suffix, err := parseMethodConfigAndSuffix(tc.in) + if err != nil { + t.Errorf("returned error %v, want nil", err) + continue + } + if s != tc.service { + t.Errorf("service = %q, want %q", s, tc.service) + } + if m != tc.method { + t.Errorf("method = %q, want %q", m, tc.method) + } + if suffix != tc.suffix { + t.Errorf("suffix = %q, want %q", suffix, tc.suffix) + } + } +} + +func TestParseMethodConfigAndSuffixInvalid(t *testing.T) { + testCases := []string{ + "*/m", + "*/m{}", + } + for _, tc := range testCases { + s, m, suffix, err := parseMethodConfigAndSuffix(tc) + if err == nil { + t.Errorf("Parsing %q got nil error with %q, %q, %q, want non-nil error", tc, s, m, suffix) + } + } +} + +func TestParseHeaderMessageLengthConfig(t *testing.T) { + testCases := []struct { + in string + hdr, msg uint64 + }{ + { + in: "", + hdr: maxUInt, msg: maxUInt, + }, + { + in: "{h}", + hdr: maxUInt, msg: 0, + }, + { + in: "{h:314}", + hdr: 314, msg: 0, + }, + { + in: "{m}", + hdr: 0, msg: maxUInt, + }, + { + in: "{m:213}", + hdr: 0, msg: 213, + }, + { + in: "{h;m}", + hdr: maxUInt, msg: maxUInt, + }, + { + in: "{h:314;m}", + hdr: 314, msg: maxUInt, + }, + { + in: "{h;m:213}", + hdr: maxUInt, msg: 213, + }, + { + in: "{h:314;m:213}", + hdr: 314, msg: 213, + }, + } + for _, tc := range testCases { + t.Logf("testing parseHeaderMessageLengthConfig(%q)", tc.in) + hdr, msg, err := parseHeaderMessageLengthConfig(tc.in) + if err != nil { + t.Errorf("returned error %v, want nil", err) + continue + } + if hdr != tc.hdr { + t.Errorf("header length = %v, want %v", hdr, tc.hdr) + } + if msg != tc.msg { + t.Errorf("message length = %v, want %v", msg, tc.msg) + } + } +} +func TestParseHeaderMessageLengthConfigInvalid(t *testing.T) { + testCases := []string{ + "{}", + "{h;a}", + "{h;m;b}", + } + for _, tc := range testCases { + _, _, err := parseHeaderMessageLengthConfig(tc) + if err == nil { + t.Errorf("Parsing %q got nil error, want non-nil error", tc) + } + } +} + +func TestFillMethodLoggerWithConfigStringBlacklist(t *testing.T) { + testCases := []string{ + "p.s/m", + "service/method", + } + for _, tc := range testCases { + c := "-" + tc + t.Logf("testing fillMethodLoggerWithConfigString(%q)", c) + l := newEmptyLogger() + if err := l.fillMethodLoggerWithConfigString(c); err != nil { + t.Errorf("returned err %v, want nil", err) + continue + } + _, ok := l.blacklist[tc] + if !ok { + t.Errorf("blacklist[%q] is not set", tc) + } + } +} + +func TestFillMethodLoggerWithConfigStringGlobal(t *testing.T) { + testCases := []struct { + in string + hdr, msg uint64 + }{ + { + in: "", + hdr: maxUInt, msg: maxUInt, + }, + { + in: "{h}", + hdr: maxUInt, msg: 0, + }, + { + in: "{h:314}", + hdr: 314, msg: 0, + }, + { + in: "{m}", + hdr: 0, msg: maxUInt, + }, + { + in: "{m:213}", + hdr: 0, msg: 213, + }, + { + in: "{h;m}", + hdr: maxUInt, msg: maxUInt, + }, + { + in: "{h:314;m}", + hdr: 314, msg: maxUInt, + }, + { + in: "{h;m:213}", + hdr: maxUInt, msg: 213, + }, + { + in: "{h:314;m:213}", + hdr: 314, msg: 213, + }, + } + for _, tc := range testCases { + c := "*" + tc.in + t.Logf("testing fillMethodLoggerWithConfigString(%q)", c) + l := newEmptyLogger() + if err := l.fillMethodLoggerWithConfigString(c); err != nil { + t.Errorf("returned err %v, want nil", err) + continue + } + if l.all == nil { + t.Errorf("l.all is not set") + continue + } + if hdr := l.all.hdr; hdr != tc.hdr { + t.Errorf("header length = %v, want %v", hdr, tc.hdr) + + } + if msg := l.all.msg; msg != tc.msg { + t.Errorf("message length = %v, want %v", msg, tc.msg) + } + } +} + +func TestFillMethodLoggerWithConfigStringPerService(t *testing.T) { + testCases := []struct { + in string + hdr, msg uint64 + }{ + { + in: "", + hdr: maxUInt, msg: maxUInt, + }, + { + in: "{h}", + hdr: maxUInt, msg: 0, + }, + { + in: "{h:314}", + hdr: 314, msg: 0, + }, + { + in: "{m}", + hdr: 0, msg: maxUInt, + }, + { + in: "{m:213}", + hdr: 0, msg: 213, + }, + { + in: "{h;m}", + hdr: maxUInt, msg: maxUInt, + }, + { + in: "{h:314;m}", + hdr: 314, msg: maxUInt, + }, + { + in: "{h;m:213}", + hdr: maxUInt, msg: 213, + }, + { + in: "{h:314;m:213}", + hdr: 314, msg: 213, + }, + } + const serviceName = "service" + for _, tc := range testCases { + c := serviceName + "/*" + tc.in + t.Logf("testing fillMethodLoggerWithConfigString(%q)", c) + l := newEmptyLogger() + if err := l.fillMethodLoggerWithConfigString(c); err != nil { + t.Errorf("returned err %v, want nil", err) + continue + } + ml, ok := l.services[serviceName] + if !ok { + t.Errorf("l.service[%q] is not set", serviceName) + continue + } + if hdr := ml.hdr; hdr != tc.hdr { + t.Errorf("header length = %v, want %v", hdr, tc.hdr) + + } + if msg := ml.msg; msg != tc.msg { + t.Errorf("message length = %v, want %v", msg, tc.msg) + } + } +} + +func TestFillMethodLoggerWithConfigStringPerMethod(t *testing.T) { + testCases := []struct { + in string + hdr, msg uint64 + }{ + { + in: "", + hdr: maxUInt, msg: maxUInt, + }, + { + in: "{h}", + hdr: maxUInt, msg: 0, + }, + { + in: "{h:314}", + hdr: 314, msg: 0, + }, + { + in: "{m}", + hdr: 0, msg: maxUInt, + }, + { + in: "{m:213}", + hdr: 0, msg: 213, + }, + { + in: "{h;m}", + hdr: maxUInt, msg: maxUInt, + }, + { + in: "{h:314;m}", + hdr: 314, msg: maxUInt, + }, + { + in: "{h;m:213}", + hdr: maxUInt, msg: 213, + }, + { + in: "{h:314;m:213}", + hdr: 314, msg: 213, + }, + } + const ( + serviceName = "service" + methodName = "method" + fullMethodName = serviceName + "/" + methodName + ) + for _, tc := range testCases { + c := fullMethodName + tc.in + t.Logf("testing fillMethodLoggerWithConfigString(%q)", c) + l := newEmptyLogger() + if err := l.fillMethodLoggerWithConfigString(c); err != nil { + t.Errorf("returned err %v, want nil", err) + continue + } + ml, ok := l.methods[fullMethodName] + if !ok { + t.Errorf("l.methods[%q] is not set", fullMethodName) + continue + } + if hdr := ml.hdr; hdr != tc.hdr { + t.Errorf("header length = %v, want %v", hdr, tc.hdr) + + } + if msg := ml.msg; msg != tc.msg { + t.Errorf("message length = %v, want %v", msg, tc.msg) + } + } +} + +func TestFillMethodLoggerWithConfigStringInvalid(t *testing.T) { + testCases := []string{ + "", + "{}", + "p.s/m{}", + "p.s/m{a}", + "p.s/m*", + "p.s/**", + "*/m", + + "-p.s/*", + "-p.s/m{h}", + } + l := &logger{} + for _, tc := range testCases { + if err := l.fillMethodLoggerWithConfigString(tc); err == nil { + t.Errorf("fillMethodLoggerWithConfigString(%q) returned nil error, want non-nil", tc) + } + } +} diff --git a/internal/binarylog/method_logger.go b/internal/binarylog/method_logger.go new file mode 100644 index 00000000..6c4462b5 --- /dev/null +++ b/internal/binarylog/method_logger.go @@ -0,0 +1,39 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package binarylog + +import ( + "io" +) + +// MethodLogger is the sub-logger for each method. +type MethodLogger struct { + headerMaxLen, messageMaxLen uint64 + + sink io.Writer // TODO(blog): make this plugable. +} + +func newMethodLogger(h, m uint64) *MethodLogger { + return &MethodLogger{ + headerMaxLen: h, + messageMaxLen: m, + + sink: nil, // TODO(blog): make it plugable. + } +} diff --git a/internal/binarylog/regexp_test.go b/internal/binarylog/regexp_test.go new file mode 100644 index 00000000..8806a5aa --- /dev/null +++ b/internal/binarylog/regexp_test.go @@ -0,0 +1,179 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package binarylog + +import ( + "reflect" + "testing" +) + +func TestLongMethodConfigRegexp(t *testing.T) { + testCases := []struct { + in string + out []string + }{ + {in: "", out: nil}, + {in: "*/m", out: nil}, + + { + in: "p.s/m{}", + out: []string{"p.s/m{}", "p.s", "m", "{}"}, + }, + + { + in: "p.s/m", + out: []string{"p.s/m", "p.s", "m", ""}, + }, + { + in: "p.s/m{h}", + out: []string{"p.s/m{h}", "p.s", "m", "{h}"}, + }, + { + in: "p.s/m{m}", + out: []string{"p.s/m{m}", "p.s", "m", "{m}"}, + }, + { + in: "p.s/m{h:123}", + out: []string{"p.s/m{h:123}", "p.s", "m", "{h:123}"}, + }, + { + in: "p.s/m{m:123}", + out: []string{"p.s/m{m:123}", "p.s", "m", "{m:123}"}, + }, + { + in: "p.s/m{h:123,m:123}", + out: []string{"p.s/m{h:123,m:123}", "p.s", "m", "{h:123,m:123}"}, + }, + + { + in: "p.s/*", + out: []string{"p.s/*", "p.s", "*", ""}, + }, + { + in: "p.s/*{h}", + out: []string{"p.s/*{h}", "p.s", "*", "{h}"}, + }, + + { + in: "s/m*", + out: []string{"s/m*", "s", "m", "*"}, + }, + { + in: "s/**", + out: []string{"s/**", "s", "*", "*"}, + }, + } + for _, tc := range testCases { + match := longMethodConfigRegexp.FindStringSubmatch(tc.in) + if !reflect.DeepEqual(match, tc.out) { + t.Errorf("in: %q, out: %q, want: %q", tc.in, match, tc.out) + } + } +} + +func TestHeaderConfigRegexp(t *testing.T) { + testCases := []struct { + in string + out []string + }{ + {in: "{}", out: nil}, + {in: "{a:b}", out: nil}, + {in: "{m:123}", out: nil}, + {in: "{h:123;m:123}", out: nil}, + + { + in: "{h}", + out: []string{"{h}", ""}, + }, + { + in: "{h:123}", + out: []string{"{h:123}", "123"}, + }, + } + for _, tc := range testCases { + match := headerConfigRegexp.FindStringSubmatch(tc.in) + if !reflect.DeepEqual(match, tc.out) { + t.Errorf("in: %q, out: %q, want: %q", tc.in, match, tc.out) + } + } +} + +func TestMessageConfigRegexp(t *testing.T) { + testCases := []struct { + in string + out []string + }{ + {in: "{}", out: nil}, + {in: "{a:b}", out: nil}, + {in: "{h:123}", out: nil}, + {in: "{h:123;m:123}", out: nil}, + + { + in: "{m}", + out: []string{"{m}", ""}, + }, + { + in: "{m:123}", + out: []string{"{m:123}", "123"}, + }, + } + for _, tc := range testCases { + match := messageConfigRegexp.FindStringSubmatch(tc.in) + if !reflect.DeepEqual(match, tc.out) { + t.Errorf("in: %q, out: %q, want: %q", tc.in, match, tc.out) + } + } +} + +func TestHeaderMessageConfigRegexp(t *testing.T) { + testCases := []struct { + in string + out []string + }{ + {in: "{}", out: nil}, + {in: "{a:b}", out: nil}, + {in: "{h}", out: nil}, + {in: "{h:123}", out: nil}, + {in: "{m}", out: nil}, + {in: "{m:123}", out: nil}, + + { + in: "{h;m}", + out: []string{"{h;m}", "", ""}, + }, + { + in: "{h:123;m}", + out: []string{"{h:123;m}", "123", ""}, + }, + { + in: "{h;m:123}", + out: []string{"{h;m:123}", "", "123"}, + }, + { + in: "{h:123;m:123}", + out: []string{"{h:123;m:123}", "123", "123"}, + }, + } + for _, tc := range testCases { + match := headerMessageConfigRegexp.FindStringSubmatch(tc.in) + if !reflect.DeepEqual(match, tc.out) { + t.Errorf("in: %q, out: %q, want: %q", tc.in, match, tc.out) + } + } +} diff --git a/internal/binarylog/util.go b/internal/binarylog/util.go new file mode 100644 index 00000000..15dc7803 --- /dev/null +++ b/internal/binarylog/util.go @@ -0,0 +1,41 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package binarylog + +import ( + "errors" + "strings" +) + +// parseMethodName splits service and method from the input. It expects format +// "/service/method". +// +// TODO: move to internal/grpcutil. +func parseMethodName(methodName string) (service, method string, _ error) { + if !strings.HasPrefix(methodName, "/") { + return "", "", errors.New("invalid method name: should start with /") + } + methodName = methodName[1:] + + pos := strings.LastIndex(methodName, "/") + if pos < 0 { + return "", "", errors.New("invalid method name: suffix /method is missing") + } + return methodName[:pos], methodName[pos+1:], nil +} diff --git a/internal/binarylog/util_test.go b/internal/binarylog/util_test.go new file mode 100644 index 00000000..3454f029 --- /dev/null +++ b/internal/binarylog/util_test.go @@ -0,0 +1,59 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package binarylog + +import "testing" + +func TestParseMethodName(t *testing.T) { + testCases := []struct { + methodName string + service, method string + }{ + {methodName: "/s/m", service: "s", method: "m"}, + {methodName: "/p.s/m", service: "p.s", method: "m"}, + {methodName: "/p/s/m", service: "p/s", method: "m"}, + } + for _, tc := range testCases { + s, m, err := parseMethodName(tc.methodName) + if err != nil { + t.Errorf("Parsing %q got error %v, want nil", tc.methodName, err) + continue + } + if s != tc.service || m != tc.method { + t.Errorf("Parseing %q got service %q, method %q, want service %q, method %q", + tc.methodName, s, m, tc.service, tc.method, + ) + } + } +} + +func TestParseMethodNameInvalid(t *testing.T) { + testCases := []string{ + "/", + "/sm", + "", + "sm", + } + for _, tc := range testCases { + _, _, err := parseMethodName(tc) + if err == nil { + t.Errorf("Parsing %q got nil error, want non-nil error", tc) + } + } +}