snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
/*
*
* 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"
"google.golang.org/grpc/internal/grpcutil"
)
var grpclogLogger = grpclog.Component("binarylog")
// Logger specifies MethodLoggers for method names with a Log call that
// takes a context.
//
// This is used in the 1.0 release of gcp/observability, and thus must not be
// deleted or changed.
type Logger interface {
GetMethodLogger(methodName string) MethodLogger
}
// binLogger is the global binary logger for the binary. One of this should be
// built at init time from the configuration (environment variable or flags).
//
// It is used to get a MethodLogger for each individual method.
var binLogger Logger
// SetLogger sets the binary logger.
//
// Only call this at init time.
func SetLogger(l Logger) {
binLogger = l
}
// GetLogger gets the binary logger.
//
// Only call this at init time.
func GetLogger() Logger {
return binLogger
}
// 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 GetMethodLogger(methodName string) MethodLogger {
if binLogger == nil {
return nil
}
return binLogger.GetMethodLogger(methodName)
}
func init() {
const envStr = "GRPC_BINARY_LOG_FILTER"
configStr := os.Getenv(envStr)
binLogger = NewLoggerFromConfigString(configStr)
}
// MethodLoggerConfig contains the setting for logging behavior of a method
// logger. Currently, it contains the max length of header and message.
type MethodLoggerConfig struct {
// Max length of header and message.
Header, Message uint64
}
// LoggerConfig contains the config for loggers to create method loggers.
type LoggerConfig struct {
All *MethodLoggerConfig
Services map[string]*MethodLoggerConfig
Methods map[string]*MethodLoggerConfig
Blacklist map[string]struct{}
}
type logger struct {
config LoggerConfig
}
// NewLoggerFromConfig builds a logger with the given LoggerConfig.
func NewLoggerFromConfig(config LoggerConfig) Logger {
return &logger{config: config}
}
// 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.config.All != nil {
return fmt.Errorf("conflicting global rules found")
}
l.config.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.config.Services[service]; ok {
return fmt.Errorf("conflicting service rules for service %v found", service)
}
if l.config.Services == nil {
l.config.Services = make(map[string]*MethodLoggerConfig)
}
l.config.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.config.Blacklist[method]; ok {
return fmt.Errorf("conflicting blacklist rules for method %v found", method)
}
if _, ok := l.config.Methods[method]; ok {
return fmt.Errorf("conflicting method rules for method %v found", method)
}
if l.config.Methods == nil {
l.config.Methods = make(map[string]*MethodLoggerConfig)
}
l.config.Methods[method] = ml
return nil
}
// Set blacklist method for "-service/method".
func (l *logger) setBlacklist(method string) error {
if _, ok := l.config.Blacklist[method]; ok {
return fmt.Errorf("conflicting blacklist rules for method %v found", method)
}
if _, ok := l.config.Methods[method]; ok {
return fmt.Errorf("conflicting method rules for method %v found", method)
}
if l.config.Blacklist == nil {
l.config.Blacklist = make(map[string]struct{})
}
l.config.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 := grpcutil.ParseMethod(methodName)
if err != nil {
grpclogLogger.Infof("binarylogging: failed to parse %q: %v", methodName, err)
return nil
}
if ml, ok := l.config.Methods[s+"/"+m]; ok {
return NewTruncatingMethodLogger(ml.Header, ml.Message)
}
if _, ok := l.config.Blacklist[s+"/"+m]; ok {
return nil
}
if ml, ok := l.config.Services[s]; ok {
return NewTruncatingMethodLogger(ml.Header, ml.Message)
}
if l.config.All == nil {
return nil
}
return NewTruncatingMethodLogger(l.config.All.Header, l.config.All.Message)
}

View File

@@ -0,0 +1,156 @@
/*
*
* 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"
"google.golang.org/grpc/internal/grpctest"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// Test that get method logger returns the one with the most exact match.
func (s) 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).(*TruncatingMethodLogger)
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 (s) 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)
}
}
}

View File

@@ -0,0 +1,42 @@
/*
*
* 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.
*
*/
// This file contains exported variables/functions that are exported for testing
// only.
//
// An ideal way for this would be to put those in a *_test.go but in binarylog
// package. But this doesn't work with staticcheck with go module. Error was:
// "MdToMetadataProto not declared by package binarylog". This could be caused
// by the way staticcheck looks for files for a certain package, which doesn't
// support *_test.go files.
//
// Move those to binary_test.go when staticcheck is fixed.
package binarylog
var (
// AllLogger is a logger that logs all headers/messages for all RPCs. It's
// for testing only.
AllLogger = NewLoggerFromConfigString("*")
// MdToMetadataProto converts metadata to a binary logging proto message.
// It's for testing only.
MdToMetadataProto = mdToMetadataProto
// AddrToProto converts an address to a binary logging proto message. It's
// for testing only.
AddrToProto = addrToProto
)

View File

@@ -0,0 +1,208 @@
/*
*
* 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"
)
// NewLoggerFromConfigString reads the string and build a logger. It can be used
// to build a new logger and assign it to binarylog.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 previous config.
func NewLoggerFromConfigString(s string) Logger {
if s == "" {
return nil
}
l := newEmptyLogger()
methods := strings.Split(s, ",")
for _, method := range methods {
if err := l.fillMethodLoggerWithConfigString(method); err != nil {
grpclogLogger.Warningf("failed to parse binary log config: %v", err)
return nil
}
}
return l
}
// fillMethodLoggerWithConfigString parses config, creates TruncatingMethodLogger 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 allowed 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{Header: hdr, Message: 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{Header: hdr, Message: msg}); err != nil {
return fmt.Errorf("invalid config: %v", err)
}
} else {
if err := l.setMethodMethodLogger(s+"/"+m, &MethodLoggerConfig{Header: hdr, Message: 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)
}

View File

@@ -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 (s) 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).(*logger)
if l.config.All.Header != 1 || l.config.All.Message != 2 {
t.Errorf("l.config.All = %#v, want headerLen: 1, messageLen: 2", l.config.All)
}
if ml, ok := l.config.Services[s1]; ok {
if ml.Header != maxUInt || ml.Message != 0 {
t.Errorf("want maxUInt header, 0 message, got header: %v, message: %v", ml.Header, ml.Message)
}
} else {
t.Errorf("service/* is not set")
}
if ml, ok := l.config.Methods[fullM1]; ok {
if ml.Header != 0 || ml.Message != maxUInt {
t.Errorf("want 0 header, maxUInt message, got header: %v, message: %v", ml.Header, ml.Message)
}
} else {
t.Errorf("service/method{h} is not set")
}
if ml, ok := l.config.Methods[fullM2]; ok {
if ml.Header != maxUInt || ml.Message != maxUInt {
t.Errorf("want maxUInt header, maxUInt message, got header: %v, message: %v", ml.Header, ml.Message)
}
} else {
t.Errorf("service/method{h;m} is not set")
}
}
func (s) TestNewLoggerFromConfigStringInvalid(t *testing.T) {
testCases := []string{
"",
"*{}",
"s/m,*{}",
"s/m,s/m{a}",
// Duplicate 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 (s) 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 (s) 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 (s) 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 (s) 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 (s) 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.config.Blacklist[tc]
if !ok {
t.Errorf("blacklist[%q] is not set", tc)
}
}
}
func (s) 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.config.All == nil {
t.Errorf("l.config.All is not set")
continue
}
if hdr := l.config.All.Header; hdr != tc.hdr {
t.Errorf("header length = %v, want %v", hdr, tc.hdr)
}
if msg := l.config.All.Message; msg != tc.msg {
t.Errorf("message length = %v, want %v", msg, tc.msg)
}
}
}
func (s) 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.config.Services[serviceName]
if !ok {
t.Errorf("l.service[%q] is not set", serviceName)
continue
}
if hdr := ml.Header; hdr != tc.hdr {
t.Errorf("header length = %v, want %v", hdr, tc.hdr)
}
if msg := ml.Message; msg != tc.msg {
t.Errorf("message length = %v, want %v", msg, tc.msg)
}
}
}
func (s) 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.config.Methods[fullMethodName]
if !ok {
t.Errorf("l.config.Methods[%q] is not set", fullMethodName)
continue
}
if hdr := ml.Header; hdr != tc.hdr {
t.Errorf("header length = %v, want %v", hdr, tc.hdr)
}
if msg := ml.Message; msg != tc.msg {
t.Errorf("message length = %v, want %v", msg, tc.msg)
}
}
}
func (s) 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)
}
}
}

View File

@@ -0,0 +1,446 @@
/*
*
* 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 (
"context"
"net"
"strings"
"sync/atomic"
"time"
binlogpb "google.golang.org/grpc/binarylog/grpc_binarylog_v1"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
type callIDGenerator struct {
id uint64
}
func (g *callIDGenerator) next() uint64 {
id := atomic.AddUint64(&g.id, 1)
return id
}
// reset is for testing only, and doesn't need to be thread safe.
func (g *callIDGenerator) reset() {
g.id = 0
}
var idGen callIDGenerator
// MethodLogger is the sub-logger for each method.
//
// This is used in the 1.0 release of gcp/observability, and thus must not be
// deleted or changed.
type MethodLogger interface {
Log(context.Context, LogEntryConfig)
}
// TruncatingMethodLogger is a method logger that truncates headers and messages
// based on configured fields.
type TruncatingMethodLogger struct {
headerMaxLen, messageMaxLen uint64
callID uint64
idWithinCallGen *callIDGenerator
sink Sink // TODO(blog): make this pluggable.
}
// NewTruncatingMethodLogger returns a new truncating method logger.
//
// This is used in the 1.0 release of gcp/observability, and thus must not be
// deleted or changed.
func NewTruncatingMethodLogger(h, m uint64) *TruncatingMethodLogger {
return &TruncatingMethodLogger{
headerMaxLen: h,
messageMaxLen: m,
callID: idGen.next(),
idWithinCallGen: &callIDGenerator{},
sink: DefaultSink, // TODO(blog): make it pluggable.
}
}
// Build is an internal only method for building the proto message out of the
// input event. It's made public to enable other library to reuse as much logic
// in TruncatingMethodLogger as possible.
func (ml *TruncatingMethodLogger) Build(c LogEntryConfig) *binlogpb.GrpcLogEntry {
m := c.toProto()
timestamp := timestamppb.Now()
m.Timestamp = timestamp
m.CallId = ml.callID
m.SequenceIdWithinCall = ml.idWithinCallGen.next()
switch pay := m.Payload.(type) {
case *binlogpb.GrpcLogEntry_ClientHeader:
m.PayloadTruncated = ml.truncateMetadata(pay.ClientHeader.GetMetadata())
case *binlogpb.GrpcLogEntry_ServerHeader:
m.PayloadTruncated = ml.truncateMetadata(pay.ServerHeader.GetMetadata())
case *binlogpb.GrpcLogEntry_Message:
m.PayloadTruncated = ml.truncateMessage(pay.Message)
}
return m
}
// Log creates a proto binary log entry, and logs it to the sink.
func (ml *TruncatingMethodLogger) Log(_ context.Context, c LogEntryConfig) {
ml.sink.Write(ml.Build(c))
}
func (ml *TruncatingMethodLogger) truncateMetadata(mdPb *binlogpb.Metadata) (truncated bool) {
if ml.headerMaxLen == maxUInt {
return false
}
var (
bytesLimit = ml.headerMaxLen
index int
)
// At the end of the loop, index will be the first entry where the total
// size is greater than the limit:
//
// len(entry[:index]) <= ml.hdr && len(entry[:index+1]) > ml.hdr.
for ; index < len(mdPb.Entry); index++ {
entry := mdPb.Entry[index]
if entry.Key == "grpc-trace-bin" {
// "grpc-trace-bin" is a special key. It's kept in the log entry,
// but not counted towards the size limit.
continue
}
currentEntryLen := uint64(len(entry.GetKey())) + uint64(len(entry.GetValue()))
if currentEntryLen > bytesLimit {
break
}
bytesLimit -= currentEntryLen
}
truncated = index < len(mdPb.Entry)
mdPb.Entry = mdPb.Entry[:index]
return truncated
}
func (ml *TruncatingMethodLogger) truncateMessage(msgPb *binlogpb.Message) (truncated bool) {
if ml.messageMaxLen == maxUInt {
return false
}
if ml.messageMaxLen >= uint64(len(msgPb.Data)) {
return false
}
msgPb.Data = msgPb.Data[:ml.messageMaxLen]
return true
}
// LogEntryConfig represents the configuration for binary log entry.
//
// This is used in the 1.0 release of gcp/observability, and thus must not be
// deleted or changed.
type LogEntryConfig interface {
toProto() *binlogpb.GrpcLogEntry
}
// ClientHeader configs the binary log entry to be a ClientHeader entry.
type ClientHeader struct {
OnClientSide bool
Header metadata.MD
MethodName string
Authority string
Timeout time.Duration
// PeerAddr is required only when it's on server side.
PeerAddr net.Addr
}
func (c *ClientHeader) toProto() *binlogpb.GrpcLogEntry {
// This function doesn't need to set all the fields (e.g. seq ID). The Log
// function will set the fields when necessary.
clientHeader := &binlogpb.ClientHeader{
Metadata: mdToMetadataProto(c.Header),
MethodName: c.MethodName,
Authority: c.Authority,
}
if c.Timeout > 0 {
clientHeader.Timeout = durationpb.New(c.Timeout)
}
ret := &binlogpb.GrpcLogEntry{
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CLIENT_HEADER,
Payload: &binlogpb.GrpcLogEntry_ClientHeader{
ClientHeader: clientHeader,
},
}
if c.OnClientSide {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_CLIENT
} else {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_SERVER
}
if c.PeerAddr != nil {
ret.Peer = addrToProto(c.PeerAddr)
}
return ret
}
// ServerHeader configs the binary log entry to be a ServerHeader entry.
type ServerHeader struct {
OnClientSide bool
Header metadata.MD
// PeerAddr is required only when it's on client side.
PeerAddr net.Addr
}
func (c *ServerHeader) toProto() *binlogpb.GrpcLogEntry {
ret := &binlogpb.GrpcLogEntry{
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_SERVER_HEADER,
Payload: &binlogpb.GrpcLogEntry_ServerHeader{
ServerHeader: &binlogpb.ServerHeader{
Metadata: mdToMetadataProto(c.Header),
},
},
}
if c.OnClientSide {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_CLIENT
} else {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_SERVER
}
if c.PeerAddr != nil {
ret.Peer = addrToProto(c.PeerAddr)
}
return ret
}
// ClientMessage configs the binary log entry to be a ClientMessage entry.
type ClientMessage struct {
OnClientSide bool
// Message can be a proto.Message or []byte. Other messages formats are not
// supported.
Message any
}
func (c *ClientMessage) toProto() *binlogpb.GrpcLogEntry {
var (
data []byte
err error
)
if m, ok := c.Message.(proto.Message); ok {
data, err = proto.Marshal(m)
if err != nil {
grpclogLogger.Infof("binarylogging: failed to marshal proto message: %v", err)
}
} else if b, ok := c.Message.([]byte); ok {
data = b
} else {
grpclogLogger.Infof("binarylogging: message to log is neither proto.message nor []byte")
}
ret := &binlogpb.GrpcLogEntry{
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CLIENT_MESSAGE,
Payload: &binlogpb.GrpcLogEntry_Message{
Message: &binlogpb.Message{
Length: uint32(len(data)),
Data: data,
},
},
}
if c.OnClientSide {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_CLIENT
} else {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_SERVER
}
return ret
}
// ServerMessage configs the binary log entry to be a ServerMessage entry.
type ServerMessage struct {
OnClientSide bool
// Message can be a proto.Message or []byte. Other messages formats are not
// supported.
Message any
}
func (c *ServerMessage) toProto() *binlogpb.GrpcLogEntry {
var (
data []byte
err error
)
if m, ok := c.Message.(proto.Message); ok {
data, err = proto.Marshal(m)
if err != nil {
grpclogLogger.Infof("binarylogging: failed to marshal proto message: %v", err)
}
} else if b, ok := c.Message.([]byte); ok {
data = b
} else {
grpclogLogger.Infof("binarylogging: message to log is neither proto.message nor []byte")
}
ret := &binlogpb.GrpcLogEntry{
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_SERVER_MESSAGE,
Payload: &binlogpb.GrpcLogEntry_Message{
Message: &binlogpb.Message{
Length: uint32(len(data)),
Data: data,
},
},
}
if c.OnClientSide {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_CLIENT
} else {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_SERVER
}
return ret
}
// ClientHalfClose configs the binary log entry to be a ClientHalfClose entry.
type ClientHalfClose struct {
OnClientSide bool
}
func (c *ClientHalfClose) toProto() *binlogpb.GrpcLogEntry {
ret := &binlogpb.GrpcLogEntry{
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CLIENT_HALF_CLOSE,
Payload: nil, // No payload here.
}
if c.OnClientSide {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_CLIENT
} else {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_SERVER
}
return ret
}
// ServerTrailer configs the binary log entry to be a ServerTrailer entry.
type ServerTrailer struct {
OnClientSide bool
Trailer metadata.MD
// Err is the status error.
Err error
// PeerAddr is required only when it's on client side and the RPC is trailer
// only.
PeerAddr net.Addr
}
func (c *ServerTrailer) toProto() *binlogpb.GrpcLogEntry {
st, ok := status.FromError(c.Err)
if !ok {
grpclogLogger.Info("binarylogging: error in trailer is not a status error")
}
var (
detailsBytes []byte
err error
)
stProto := st.Proto()
if stProto != nil && len(stProto.Details) != 0 {
detailsBytes, err = proto.Marshal(stProto)
if err != nil {
grpclogLogger.Infof("binarylogging: failed to marshal status proto: %v", err)
}
}
ret := &binlogpb.GrpcLogEntry{
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_SERVER_TRAILER,
Payload: &binlogpb.GrpcLogEntry_Trailer{
Trailer: &binlogpb.Trailer{
Metadata: mdToMetadataProto(c.Trailer),
StatusCode: uint32(st.Code()),
StatusMessage: st.Message(),
StatusDetails: detailsBytes,
},
},
}
if c.OnClientSide {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_CLIENT
} else {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_SERVER
}
if c.PeerAddr != nil {
ret.Peer = addrToProto(c.PeerAddr)
}
return ret
}
// Cancel configs the binary log entry to be a Cancel entry.
type Cancel struct {
OnClientSide bool
}
func (c *Cancel) toProto() *binlogpb.GrpcLogEntry {
ret := &binlogpb.GrpcLogEntry{
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CANCEL,
Payload: nil,
}
if c.OnClientSide {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_CLIENT
} else {
ret.Logger = binlogpb.GrpcLogEntry_LOGGER_SERVER
}
return ret
}
// metadataKeyOmit returns whether the metadata entry with this key should be
// omitted.
func metadataKeyOmit(key string) bool {
switch key {
case "lb-token", ":path", ":authority", "content-encoding", "content-type", "user-agent", "te", "authorization":
return true
case "grpc-trace-bin": // grpc-trace-bin is special because it's visible to users.
return false
}
return strings.HasPrefix(key, "grpc-")
}
func mdToMetadataProto(md metadata.MD) *binlogpb.Metadata {
ret := &binlogpb.Metadata{}
for k, vv := range md {
if metadataKeyOmit(k) {
continue
}
for _, v := range vv {
ret.Entry = append(ret.Entry,
&binlogpb.MetadataEntry{
Key: k,
Value: []byte(v),
},
)
}
}
return ret
}
func addrToProto(addr net.Addr) *binlogpb.Address {
ret := &binlogpb.Address{}
switch a := addr.(type) {
case *net.TCPAddr:
if a.IP.To4() != nil {
ret.Type = binlogpb.Address_TYPE_IPV4
} else if a.IP.To16() != nil {
ret.Type = binlogpb.Address_TYPE_IPV6
} else {
ret.Type = binlogpb.Address_TYPE_UNKNOWN
// Do not set address and port fields.
break
}
ret.Address = a.IP.String()
ret.IpPort = uint32(a.Port)
case *net.UnixAddr:
ret.Type = binlogpb.Address_TYPE_UNIX
ret.Address = a.String()
default:
ret.Type = binlogpb.Address_TYPE_UNKNOWN
}
return ret
}

View File

@@ -0,0 +1,562 @@
/*
*
* 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 (
"bytes"
"context"
"fmt"
"net"
"testing"
"time"
binlogpb "google.golang.org/grpc/binarylog/grpc_binarylog_v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
)
const defaultTestTimeout = 10 * time.Second
func (s) TestLog(t *testing.T) {
idGen.reset()
ml := NewTruncatingMethodLogger(10, 10)
// Set sink to testing buffer.
buf := bytes.NewBuffer(nil)
ml.sink = newWriterSink(buf)
addr := "1.2.3.4"
port := 790
tcpAddr, _ := net.ResolveTCPAddr("tcp", fmt.Sprintf("%v:%d", addr, port))
addr6 := "2001:1db8:85a3::8a2e:1370:7334"
port6 := 796
tcpAddr6, _ := net.ResolveTCPAddr("tcp", fmt.Sprintf("[%v]:%d", addr6, port6))
testProtoMsg := &binlogpb.Message{
Length: 1,
Data: []byte{'a'},
}
testProtoBytes, _ := proto.Marshal(testProtoMsg)
testCases := []struct {
config LogEntryConfig
want *binlogpb.GrpcLogEntry
}{
{
config: &ClientHeader{
OnClientSide: false,
Header: map[string][]string{
"a": {"b", "bb"},
},
MethodName: "testservice/testmethod",
Authority: "test.service.io",
Timeout: 2*time.Second + 3*time.Nanosecond,
PeerAddr: tcpAddr,
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CLIENT_HEADER,
Logger: binlogpb.GrpcLogEntry_LOGGER_SERVER,
Payload: &binlogpb.GrpcLogEntry_ClientHeader{
ClientHeader: &binlogpb.ClientHeader{
Metadata: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "a", Value: []byte{'b'}},
{Key: "a", Value: []byte{'b', 'b'}},
},
},
MethodName: "testservice/testmethod",
Authority: "test.service.io",
Timeout: &durationpb.Duration{
Seconds: 2,
Nanos: 3,
},
},
},
PayloadTruncated: false,
Peer: &binlogpb.Address{
Type: binlogpb.Address_TYPE_IPV4,
Address: addr,
IpPort: uint32(port),
},
},
},
{
config: &ClientHeader{
OnClientSide: false,
MethodName: "testservice/testmethod",
Authority: "test.service.io",
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CLIENT_HEADER,
Logger: binlogpb.GrpcLogEntry_LOGGER_SERVER,
Payload: &binlogpb.GrpcLogEntry_ClientHeader{
ClientHeader: &binlogpb.ClientHeader{
Metadata: &binlogpb.Metadata{},
MethodName: "testservice/testmethod",
Authority: "test.service.io",
},
},
PayloadTruncated: false,
},
},
{
config: &ServerHeader{
OnClientSide: true,
Header: map[string][]string{
"a": {"b", "bb"},
},
PeerAddr: tcpAddr6,
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_SERVER_HEADER,
Logger: binlogpb.GrpcLogEntry_LOGGER_CLIENT,
Payload: &binlogpb.GrpcLogEntry_ServerHeader{
ServerHeader: &binlogpb.ServerHeader{
Metadata: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "a", Value: []byte{'b'}},
{Key: "a", Value: []byte{'b', 'b'}},
},
},
},
},
PayloadTruncated: false,
Peer: &binlogpb.Address{
Type: binlogpb.Address_TYPE_IPV6,
Address: addr6,
IpPort: uint32(port6),
},
},
},
{
config: &ClientMessage{
OnClientSide: true,
Message: testProtoMsg,
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CLIENT_MESSAGE,
Logger: binlogpb.GrpcLogEntry_LOGGER_CLIENT,
Payload: &binlogpb.GrpcLogEntry_Message{
Message: &binlogpb.Message{
Length: uint32(len(testProtoBytes)),
Data: testProtoBytes,
},
},
PayloadTruncated: false,
Peer: nil,
},
},
{
config: &ServerMessage{
OnClientSide: false,
Message: testProtoMsg,
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_SERVER_MESSAGE,
Logger: binlogpb.GrpcLogEntry_LOGGER_SERVER,
Payload: &binlogpb.GrpcLogEntry_Message{
Message: &binlogpb.Message{
Length: uint32(len(testProtoBytes)),
Data: testProtoBytes,
},
},
PayloadTruncated: false,
Peer: nil,
},
},
{
config: &ClientHalfClose{
OnClientSide: false,
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CLIENT_HALF_CLOSE,
Logger: binlogpb.GrpcLogEntry_LOGGER_SERVER,
Payload: nil,
PayloadTruncated: false,
Peer: nil,
},
},
{
config: &ServerTrailer{
OnClientSide: true,
Err: status.Errorf(codes.Unavailable, "test"),
PeerAddr: tcpAddr,
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_SERVER_TRAILER,
Logger: binlogpb.GrpcLogEntry_LOGGER_CLIENT,
Payload: &binlogpb.GrpcLogEntry_Trailer{
Trailer: &binlogpb.Trailer{
Metadata: &binlogpb.Metadata{},
StatusCode: uint32(codes.Unavailable),
StatusMessage: "test",
StatusDetails: nil,
},
},
PayloadTruncated: false,
Peer: &binlogpb.Address{
Type: binlogpb.Address_TYPE_IPV4,
Address: addr,
IpPort: uint32(port),
},
},
},
{ // Err is nil, Log OK status.
config: &ServerTrailer{
OnClientSide: true,
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_SERVER_TRAILER,
Logger: binlogpb.GrpcLogEntry_LOGGER_CLIENT,
Payload: &binlogpb.GrpcLogEntry_Trailer{
Trailer: &binlogpb.Trailer{
Metadata: &binlogpb.Metadata{},
StatusCode: uint32(codes.OK),
StatusMessage: "",
StatusDetails: nil,
},
},
PayloadTruncated: false,
Peer: nil,
},
},
{
config: &Cancel{
OnClientSide: true,
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CANCEL,
Logger: binlogpb.GrpcLogEntry_LOGGER_CLIENT,
Payload: nil,
PayloadTruncated: false,
Peer: nil,
},
},
// gRPC headers should be omitted.
{
config: &ClientHeader{
OnClientSide: false,
Header: map[string][]string{
"lb-token": {"to be omitted"},
":path": {"to be omitted"},
":authority": {"to be omitted"},
"content-encoding": {"to be omitted"},
"content-type": {"to be omitted"},
"user-agent": {"to be omitted"},
"te": {"to be omitted"},
"authorization": {"Bearer to-be-omitted"},
"grpc-reserved": {"to be omitted"},
"a": {"b", "bb"},
},
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_CLIENT_HEADER,
Logger: binlogpb.GrpcLogEntry_LOGGER_SERVER,
Payload: &binlogpb.GrpcLogEntry_ClientHeader{
ClientHeader: &binlogpb.ClientHeader{
Metadata: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "a", Value: []byte{'b'}},
{Key: "a", Value: []byte{'b', 'b'}},
},
},
},
},
PayloadTruncated: false,
},
},
{
config: &ServerHeader{
OnClientSide: true,
Header: map[string][]string{
"lb-token": {"to be omitted"},
":path": {"to be omitted"},
":authority": {"to be omitted"},
"content-encoding": {"to be omitted"},
"content-type": {"to be omitted"},
"user-agent": {"to be omitted"},
"te": {"to be omitted"},
"authorization": {"Bearer to-be-omitted"},
"grpc-reserved": {"to be omitted"},
"a": {"b", "bb"},
},
},
want: &binlogpb.GrpcLogEntry{
Timestamp: nil,
CallId: 1,
SequenceIdWithinCall: 0,
Type: binlogpb.GrpcLogEntry_EVENT_TYPE_SERVER_HEADER,
Logger: binlogpb.GrpcLogEntry_LOGGER_CLIENT,
Payload: &binlogpb.GrpcLogEntry_ServerHeader{
ServerHeader: &binlogpb.ServerHeader{
Metadata: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "a", Value: []byte{'b'}},
{Key: "a", Value: []byte{'b', 'b'}},
},
},
},
},
PayloadTruncated: false,
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for i, tc := range testCases {
buf.Reset()
tc.want.SequenceIdWithinCall = uint64(i + 1)
ml.Log(ctx, tc.config)
inSink := new(binlogpb.GrpcLogEntry)
if err := proto.Unmarshal(buf.Bytes()[4:], inSink); err != nil {
t.Errorf("failed to unmarshal bytes in sink to proto: %v", err)
continue
}
inSink.Timestamp = nil // Strip timestamp before comparing.
if !proto.Equal(inSink, tc.want) {
t.Errorf("Log(%+v), in sink: %+v, want %+v", tc.config, inSink, tc.want)
}
}
}
func (s) TestTruncateMetadataNotTruncated(t *testing.T) {
testCases := []struct {
ml *TruncatingMethodLogger
mpPb *binlogpb.Metadata
}{
{
ml: NewTruncatingMethodLogger(maxUInt, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: []byte{1}},
},
},
},
{
ml: NewTruncatingMethodLogger(2, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: []byte{1}},
},
},
},
{
ml: NewTruncatingMethodLogger(1, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: nil},
},
},
},
{
ml: NewTruncatingMethodLogger(2, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: []byte{1, 1}},
},
},
},
{
ml: NewTruncatingMethodLogger(2, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: []byte{1}},
{Key: "", Value: []byte{1}},
},
},
},
// "grpc-trace-bin" is kept in log but not counted towards the size
// limit.
{
ml: NewTruncatingMethodLogger(1, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: []byte{1}},
{Key: "grpc-trace-bin", Value: []byte("some.trace.key")},
},
},
},
}
for i, tc := range testCases {
truncated := tc.ml.truncateMetadata(tc.mpPb)
if truncated {
t.Errorf("test case %v, returned truncated, want not truncated", i)
}
}
}
func (s) TestTruncateMetadataTruncated(t *testing.T) {
testCases := []struct {
ml *TruncatingMethodLogger
mpPb *binlogpb.Metadata
entryLen int
}{
{
ml: NewTruncatingMethodLogger(2, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: []byte{1, 1, 1}},
},
},
entryLen: 0,
},
{
ml: NewTruncatingMethodLogger(2, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: []byte{1}},
{Key: "", Value: []byte{1}},
{Key: "", Value: []byte{1}},
},
},
entryLen: 2,
},
{
ml: NewTruncatingMethodLogger(2, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: []byte{1, 1}},
{Key: "", Value: []byte{1}},
},
},
entryLen: 1,
},
{
ml: NewTruncatingMethodLogger(2, maxUInt),
mpPb: &binlogpb.Metadata{
Entry: []*binlogpb.MetadataEntry{
{Key: "", Value: []byte{1}},
{Key: "", Value: []byte{1, 1}},
},
},
entryLen: 1,
},
}
for i, tc := range testCases {
truncated := tc.ml.truncateMetadata(tc.mpPb)
if !truncated {
t.Errorf("test case %v, returned not truncated, want truncated", i)
continue
}
if len(tc.mpPb.Entry) != tc.entryLen {
t.Errorf("test case %v, entry length: %v, want: %v", i, len(tc.mpPb.Entry), tc.entryLen)
}
}
}
func (s) TestTruncateMessageNotTruncated(t *testing.T) {
testCases := []struct {
ml *TruncatingMethodLogger
msgPb *binlogpb.Message
}{
{
ml: NewTruncatingMethodLogger(maxUInt, maxUInt),
msgPb: &binlogpb.Message{
Data: []byte{1},
},
},
{
ml: NewTruncatingMethodLogger(maxUInt, 3),
msgPb: &binlogpb.Message{
Data: []byte{1, 1},
},
},
{
ml: NewTruncatingMethodLogger(maxUInt, 2),
msgPb: &binlogpb.Message{
Data: []byte{1, 1},
},
},
}
for i, tc := range testCases {
truncated := tc.ml.truncateMessage(tc.msgPb)
if truncated {
t.Errorf("test case %v, returned truncated, want not truncated", i)
}
}
}
func (s) TestTruncateMessageTruncated(t *testing.T) {
testCases := []struct {
ml *TruncatingMethodLogger
msgPb *binlogpb.Message
oldLength uint32
}{
{
ml: NewTruncatingMethodLogger(maxUInt, 2),
msgPb: &binlogpb.Message{
Length: 3,
Data: []byte{1, 1, 1},
},
oldLength: 3,
},
}
for i, tc := range testCases {
truncated := tc.ml.truncateMessage(tc.msgPb)
if !truncated {
t.Errorf("test case %v, returned not truncated, want truncated", i)
continue
}
if len(tc.msgPb.Data) != int(tc.ml.messageMaxLen) {
t.Errorf("test case %v, message length: %v, want: %v", i, len(tc.msgPb.Data), tc.ml.messageMaxLen)
}
if tc.msgPb.Length != tc.oldLength {
t.Errorf("test case %v, message.Length field: %v, want: %v", i, tc.msgPb.Length, tc.oldLength)
}
}
}

View File

@@ -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 (s) 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 (s) 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 (s) 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 (s) 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)
}
}
}

170
internal/binarylog/sink.go Normal file
View File

@@ -0,0 +1,170 @@
/*
*
* 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 (
"bufio"
"encoding/binary"
"io"
"sync"
"time"
binlogpb "google.golang.org/grpc/binarylog/grpc_binarylog_v1"
"google.golang.org/protobuf/proto"
)
var (
// DefaultSink is the sink where the logs will be written to. It's exported
// for the binarylog package to update.
DefaultSink Sink = &noopSink{} // TODO(blog): change this default (file in /tmp).
)
// Sink writes log entry into the binary log sink.
//
// sink is a copy of the exported binarylog.Sink, to avoid circular dependency.
type Sink interface {
// Write will be called to write the log entry into the sink.
//
// It should be thread-safe so it can be called in parallel.
Write(*binlogpb.GrpcLogEntry) error
// Close will be called when the Sink is replaced by a new Sink.
Close() error
}
type noopSink struct{}
func (ns *noopSink) Write(*binlogpb.GrpcLogEntry) error { return nil }
func (ns *noopSink) Close() error { return nil }
// newWriterSink creates a binary log sink with the given writer.
//
// Write() marshals the proto message and writes it to the given writer. Each
// message is prefixed with a 4 byte big endian unsigned integer as the length.
//
// No buffer is done, Close() doesn't try to close the writer.
func newWriterSink(w io.Writer) Sink {
return &writerSink{out: w}
}
type writerSink struct {
out io.Writer
}
func (ws *writerSink) Write(e *binlogpb.GrpcLogEntry) error {
b, err := proto.Marshal(e)
if err != nil {
grpclogLogger.Errorf("binary logging: failed to marshal proto message: %v", err)
return err
}
hdr := make([]byte, 4)
binary.BigEndian.PutUint32(hdr, uint32(len(b)))
if _, err := ws.out.Write(hdr); err != nil {
return err
}
if _, err := ws.out.Write(b); err != nil {
return err
}
return nil
}
func (ws *writerSink) Close() error { return nil }
type bufferedSink struct {
mu sync.Mutex
closer io.Closer
out Sink // out is built on buf.
buf *bufio.Writer // buf is kept for flush.
flusherStarted bool
writeTicker *time.Ticker
done chan struct{}
}
func (fs *bufferedSink) Write(e *binlogpb.GrpcLogEntry) error {
fs.mu.Lock()
defer fs.mu.Unlock()
if !fs.flusherStarted {
// Start the write loop when Write is called.
fs.startFlushGoroutine()
fs.flusherStarted = true
}
if err := fs.out.Write(e); err != nil {
return err
}
return nil
}
const (
bufFlushDuration = 60 * time.Second
)
func (fs *bufferedSink) startFlushGoroutine() {
fs.writeTicker = time.NewTicker(bufFlushDuration)
go func() {
for {
select {
case <-fs.done:
return
case <-fs.writeTicker.C:
}
fs.mu.Lock()
if err := fs.buf.Flush(); err != nil {
grpclogLogger.Warningf("failed to flush to Sink: %v", err)
}
fs.mu.Unlock()
}
}()
}
func (fs *bufferedSink) Close() error {
fs.mu.Lock()
defer fs.mu.Unlock()
if fs.writeTicker != nil {
fs.writeTicker.Stop()
}
close(fs.done)
if err := fs.buf.Flush(); err != nil {
grpclogLogger.Warningf("failed to flush to Sink: %v", err)
}
if err := fs.closer.Close(); err != nil {
grpclogLogger.Warningf("failed to close the underlying WriterCloser: %v", err)
}
if err := fs.out.Close(); err != nil {
grpclogLogger.Warningf("failed to close the Sink: %v", err)
}
return nil
}
// NewBufferedSink creates a binary log sink with the given WriteCloser.
//
// Write() marshals the proto message and writes it to the given writer. Each
// message is prefixed with a 4 byte big endian unsigned integer as the length.
//
// Content is kept in a buffer, and is flushed every 60 seconds.
//
// Close closes the WriteCloser.
func NewBufferedSink(o io.WriteCloser) Sink {
bufW := bufio.NewWriter(o)
return &bufferedSink{
closer: o,
out: newWriterSink(bufW),
buf: bufW,
done: make(chan struct{}),
}
}