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
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
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
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (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
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
Stale bot / stale (push) Failing after 1s
This commit is contained in:
47
experimental/balancer/hostname/hostname.go
Normal file
47
experimental/balancer/hostname/hostname.go
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2026 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 hostname contains utilities for the endpoint hostname attribute
|
||||
// (used for per-endpoint :authority / SNI override).
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: All APIs in this package are EXPERIMENTAL and may be changed
|
||||
// or removed in a later release.
|
||||
package hostname
|
||||
|
||||
import "google.golang.org/grpc/resolver"
|
||||
|
||||
type hostnameKey struct{}
|
||||
|
||||
// Set returns a copy of the given endpoint with the hostname attribute
|
||||
// set. If hostname is empty the endpoint is returned unmodified.
|
||||
func Set(endpoint resolver.Endpoint, hostname string) resolver.Endpoint {
|
||||
if hostname == "" {
|
||||
return endpoint
|
||||
}
|
||||
endpoint.Attributes = endpoint.Attributes.WithValue(hostnameKey{}, hostname)
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// FromEndpoint returns the hostname attribute of endpoint. If this
|
||||
// attribute is not set, it returns the empty string.
|
||||
func FromEndpoint(endpoint resolver.Endpoint) string {
|
||||
h, _ := endpoint.Attributes.Value(hostnameKey{}).(string)
|
||||
return h
|
||||
}
|
||||
75
experimental/balancer/hostname/hostname_test.go
Normal file
75
experimental/balancer/hostname/hostname_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2026 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 hostname_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/attributes"
|
||||
"google.golang.org/grpc/experimental/balancer/hostname"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func (s) TestHostnameToAndFromEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inputHostname string
|
||||
inputAttributes *attributes.Attributes
|
||||
wantHostname string
|
||||
}{
|
||||
{
|
||||
desc: "empty_attributes",
|
||||
inputHostname: "myservice.example.com",
|
||||
wantHostname: "myservice.example.com",
|
||||
},
|
||||
{
|
||||
desc: "non-empty_attributes",
|
||||
inputHostname: "myservice.example.com",
|
||||
inputAttributes: attributes.New("foo", "bar"),
|
||||
wantHostname: "myservice.example.com",
|
||||
},
|
||||
{
|
||||
desc: "hostname_not_present_in_empty_attributes",
|
||||
},
|
||||
{
|
||||
desc: "hostname_not_present_in_non-empty_attributes",
|
||||
inputAttributes: attributes.New("foo", "bar"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
endpoint := resolver.Endpoint{Attributes: test.inputAttributes}
|
||||
endpoint = hostname.Set(endpoint, test.inputHostname)
|
||||
gotHostname := hostname.FromEndpoint(endpoint)
|
||||
if gotHostname != test.wantHostname {
|
||||
t.Errorf("hostname.FromEndpoint(%+v) = %q, want %q", endpoint, gotHostname, test.wantHostname)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
60
experimental/balancer/weight/weight.go
Normal file
60
experimental/balancer/weight/weight.go
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 weight contains utilities to manage endpoint weights.
|
||||
// Weights may be used by LB policies to distribute load across
|
||||
// multiple endpoints.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: All APIs in this package are EXPERIMENTAL and may be changed
|
||||
// or removed in a later release.
|
||||
package weight
|
||||
|
||||
import "google.golang.org/grpc/resolver"
|
||||
|
||||
// attributeKey is the type used as the key to store EndpointInfo in the
|
||||
// Attributes field of resolver.Endpoint.
|
||||
type attributeKey struct{}
|
||||
|
||||
// EndpointInfo will be stored in the Attributes field of Endpoints.
|
||||
type EndpointInfo struct {
|
||||
Weight uint32
|
||||
}
|
||||
|
||||
// Equal allows the values to be compared by Attributes.Equal.
|
||||
func (a EndpointInfo) Equal(o any) bool {
|
||||
oa, ok := o.(EndpointInfo)
|
||||
return ok && oa.Weight == a.Weight
|
||||
}
|
||||
|
||||
// Set returns a copy of endpoint in which the Attributes field is
|
||||
// updated with EndpointInfo.
|
||||
func Set(endpoint resolver.Endpoint, epInfo EndpointInfo) resolver.Endpoint {
|
||||
endpoint.Attributes = endpoint.Attributes.WithValue(attributeKey{}, epInfo)
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// FromEndpoint returns the EndpointInfo stored in the Attributes
|
||||
// field of an endpoint. It returns an empty EndpointInfo if attribute
|
||||
// is not found.
|
||||
func FromEndpoint(endpoint resolver.Endpoint) EndpointInfo {
|
||||
v := endpoint.Attributes.Value(attributeKey{})
|
||||
ei, _ := v.(EndpointInfo)
|
||||
return ei
|
||||
}
|
||||
83
experimental/balancer/weight/weight_test.go
Normal file
83
experimental/balancer/weight/weight_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 weight_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/attributes"
|
||||
"google.golang.org/grpc/experimental/balancer/weight"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func (s) TestEndpointInfoToAndFromAttributes(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inputEndpointInfo weight.EndpointInfo
|
||||
inputAttributes *attributes.Attributes
|
||||
wantEndpointInfo weight.EndpointInfo
|
||||
}{
|
||||
{
|
||||
desc: "empty_attributes",
|
||||
inputEndpointInfo: weight.EndpointInfo{Weight: 100},
|
||||
inputAttributes: nil,
|
||||
wantEndpointInfo: weight.EndpointInfo{Weight: 100},
|
||||
},
|
||||
{
|
||||
desc: "non-empty_attributes",
|
||||
inputEndpointInfo: weight.EndpointInfo{Weight: 100},
|
||||
inputAttributes: attributes.New("foo", "bar"),
|
||||
wantEndpointInfo: weight.EndpointInfo{Weight: 100},
|
||||
},
|
||||
{
|
||||
desc: "endpointInfo_not_present_in_empty_attributes",
|
||||
inputEndpointInfo: weight.EndpointInfo{},
|
||||
inputAttributes: nil,
|
||||
wantEndpointInfo: weight.EndpointInfo{},
|
||||
},
|
||||
{
|
||||
desc: "endpointInfo_not_present_in_non-empty_attributes",
|
||||
inputEndpointInfo: weight.EndpointInfo{},
|
||||
inputAttributes: attributes.New("foo", "bar"),
|
||||
wantEndpointInfo: weight.EndpointInfo{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
endpoint := resolver.Endpoint{Attributes: test.inputAttributes}
|
||||
endpoint = weight.Set(endpoint, test.inputEndpointInfo)
|
||||
gotEndpointInfo := weight.FromEndpoint(endpoint)
|
||||
if !cmp.Equal(gotEndpointInfo, test.wantEndpointInfo) {
|
||||
t.Errorf("gotEndpointInfo: %v, wantEndpointInfo: %v", gotEndpointInfo, test.wantEndpointInfo)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
252
experimental/credentials/credentials_test.go
Normal file
252
experimental/credentials/credentials_test.go
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 credentials
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/testdata"
|
||||
)
|
||||
|
||||
const defaultTestTimeout = 10 * time.Second
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func (s) TestTLSOverrideServerName(t *testing.T) {
|
||||
expectedServerName := "server.name"
|
||||
c := NewTLSWithALPNDisabled(nil)
|
||||
c.OverrideServerName(expectedServerName)
|
||||
if c.Info().ServerName != expectedServerName {
|
||||
t.Fatalf("c.Info().ServerName = %v, want %v", c.Info().ServerName, expectedServerName)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestTLSClone(t *testing.T) {
|
||||
expectedServerName := "server.name"
|
||||
c := NewTLSWithALPNDisabled(nil)
|
||||
c.OverrideServerName(expectedServerName)
|
||||
cc := c.Clone()
|
||||
if cc.Info().ServerName != expectedServerName {
|
||||
t.Fatalf("cc.Info().ServerName = %v, want %v", cc.Info().ServerName, expectedServerName)
|
||||
}
|
||||
cc.OverrideServerName("")
|
||||
if c.Info().ServerName != expectedServerName {
|
||||
t.Fatalf("Change in clone should not affect the original, c.Info().ServerName = %v, want %v", c.Info().ServerName, expectedServerName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type serverHandshake func(net.Conn) (credentials.AuthInfo, error)
|
||||
|
||||
func (s) TestClientHandshakeReturnsAuthInfo(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
address string
|
||||
}{
|
||||
{
|
||||
name: "localhost",
|
||||
address: "localhost:0",
|
||||
},
|
||||
{
|
||||
name: "ipv4",
|
||||
address: "127.0.0.1:0",
|
||||
},
|
||||
{
|
||||
name: "ipv6",
|
||||
address: "[::1]:0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
done := make(chan credentials.AuthInfo, 1)
|
||||
lis := launchServerOnListenAddress(t, tlsServerHandshake, done, tc.address)
|
||||
defer lis.Close()
|
||||
lisAddr := lis.Addr().String()
|
||||
clientAuthInfo := clientHandle(t, gRPCClientHandshake, lisAddr)
|
||||
// wait until server sends serverAuthInfo or fails.
|
||||
serverAuthInfo, ok := <-done
|
||||
if !ok {
|
||||
t.Fatalf("Error at server-side")
|
||||
}
|
||||
if !compare(clientAuthInfo, serverAuthInfo) {
|
||||
t.Fatalf("c.ClientHandshake(_, %v, _) = %v, want %v.", lisAddr, clientAuthInfo, serverAuthInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestServerHandshakeReturnsAuthInfo(t *testing.T) {
|
||||
done := make(chan credentials.AuthInfo, 1)
|
||||
lis := launchServer(t, gRPCServerHandshake, done)
|
||||
defer lis.Close()
|
||||
clientAuthInfo := clientHandle(t, tlsClientHandshake, lis.Addr().String())
|
||||
// wait until server sends serverAuthInfo or fails.
|
||||
serverAuthInfo, ok := <-done
|
||||
if !ok {
|
||||
t.Fatalf("Error at server-side")
|
||||
}
|
||||
if !compare(clientAuthInfo, serverAuthInfo) {
|
||||
t.Fatalf("ServerHandshake(_) = %v, want %v.", serverAuthInfo, clientAuthInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestServerAndClientHandshake(t *testing.T) {
|
||||
done := make(chan credentials.AuthInfo, 1)
|
||||
lis := launchServer(t, gRPCServerHandshake, done)
|
||||
defer lis.Close()
|
||||
clientAuthInfo := clientHandle(t, gRPCClientHandshake, lis.Addr().String())
|
||||
// wait until server sends serverAuthInfo or fails.
|
||||
serverAuthInfo, ok := <-done
|
||||
if !ok {
|
||||
t.Fatalf("Error at server-side")
|
||||
}
|
||||
if !compare(clientAuthInfo, serverAuthInfo) {
|
||||
t.Fatalf("AuthInfo returned by server: %v and client: %v aren't same", serverAuthInfo, clientAuthInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func compare(a1, a2 credentials.AuthInfo) bool {
|
||||
if a1.AuthType() != a2.AuthType() {
|
||||
return false
|
||||
}
|
||||
switch a1.AuthType() {
|
||||
case "tls":
|
||||
state1 := a1.(credentials.TLSInfo).State
|
||||
state2 := a2.(credentials.TLSInfo).State
|
||||
if state1.Version == state2.Version &&
|
||||
state1.HandshakeComplete == state2.HandshakeComplete &&
|
||||
state1.CipherSuite == state2.CipherSuite &&
|
||||
state1.NegotiatedProtocol == state2.NegotiatedProtocol {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func launchServer(t *testing.T, hs serverHandshake, done chan credentials.AuthInfo) net.Listener {
|
||||
return launchServerOnListenAddress(t, hs, done, "localhost:0")
|
||||
}
|
||||
|
||||
func launchServerOnListenAddress(t *testing.T, hs serverHandshake, done chan credentials.AuthInfo, address string) net.Listener {
|
||||
lis, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "bind: cannot assign requested address") ||
|
||||
strings.Contains(err.Error(), "socket: address family not supported by protocol") {
|
||||
t.Skipf("no support for address %v", address)
|
||||
}
|
||||
t.Fatalf("Failed to listen: %v", err)
|
||||
}
|
||||
go serverHandle(t, hs, done, lis)
|
||||
return lis
|
||||
}
|
||||
|
||||
// Is run in a separate goroutine.
|
||||
func serverHandle(t *testing.T, hs serverHandshake, done chan credentials.AuthInfo, lis net.Listener) {
|
||||
serverRawConn, err := lis.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Server failed to accept connection: %v", err)
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
serverAuthInfo, err := hs(serverRawConn)
|
||||
if err != nil {
|
||||
t.Errorf("Server failed while handshake. Error: %v", err)
|
||||
serverRawConn.Close()
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
done <- serverAuthInfo
|
||||
}
|
||||
|
||||
func clientHandle(t *testing.T, hs func(net.Conn, string) (credentials.AuthInfo, error), lisAddr string) credentials.AuthInfo {
|
||||
conn, err := net.Dial("tcp", lisAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("Client failed to connect to %s. Error: %v", lisAddr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
clientAuthInfo, err := hs(conn, lisAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("Error on client while handshake. Error: %v", err)
|
||||
}
|
||||
return clientAuthInfo
|
||||
}
|
||||
|
||||
// Server handshake implementation in gRPC.
|
||||
func gRPCServerHandshake(conn net.Conn) (credentials.AuthInfo, error) {
|
||||
serverTLS, err := NewServerTLSFromFileWithALPNDisabled(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, serverAuthInfo, err := serverTLS.ServerHandshake(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return serverAuthInfo, nil
|
||||
}
|
||||
|
||||
// Client handshake implementation in gRPC.
|
||||
func gRPCClientHandshake(conn net.Conn, lisAddr string) (credentials.AuthInfo, error) {
|
||||
clientTLS := NewTLSWithALPNDisabled(&tls.Config{InsecureSkipVerify: true})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
_, authInfo, err := clientTLS.ClientHandshake(ctx, lisAddr, conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authInfo, nil
|
||||
}
|
||||
|
||||
func tlsServerHandshake(conn net.Conn) (credentials.AuthInfo, error) {
|
||||
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverTLSConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
serverConn := tls.Server(conn, serverTLSConfig)
|
||||
err = serverConn.Handshake()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return credentials.TLSInfo{State: serverConn.ConnectionState(), CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}}, nil
|
||||
}
|
||||
|
||||
func tlsClientHandshake(conn net.Conn, _ string) (credentials.AuthInfo, error) {
|
||||
clientTLSConfig := &tls.Config{InsecureSkipVerify: true}
|
||||
clientConn := tls.Client(conn, clientTLSConfig)
|
||||
if err := clientConn.Handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return credentials.TLSInfo{State: clientConn.ConnectionState(), CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}}, nil
|
||||
}
|
||||
75
experimental/credentials/internal/spiffe.go
Normal file
75
experimental/credentials/internal/spiffe.go
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 internal defines APIs for parsing SPIFFE ID.
|
||||
//
|
||||
// All APIs in this package are experimental.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/url"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
)
|
||||
|
||||
var logger = grpclog.Component("credentials")
|
||||
|
||||
// SPIFFEIDFromState parses the SPIFFE ID from State. If the SPIFFE ID format
|
||||
// is invalid, return nil with warning.
|
||||
func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
|
||||
if len(state.PeerCertificates) == 0 || len(state.PeerCertificates[0].URIs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return SPIFFEIDFromCert(state.PeerCertificates[0])
|
||||
}
|
||||
|
||||
// SPIFFEIDFromCert parses the SPIFFE ID from x509.Certificate. If the SPIFFE
|
||||
// ID format is invalid, return nil with warning.
|
||||
func SPIFFEIDFromCert(cert *x509.Certificate) *url.URL {
|
||||
if cert == nil || cert.URIs == nil {
|
||||
return nil
|
||||
}
|
||||
var spiffeID *url.URL
|
||||
for _, uri := range cert.URIs {
|
||||
if uri == nil || uri.Scheme != "spiffe" || uri.Opaque != "" || (uri.User != nil && uri.User.Username() != "") {
|
||||
continue
|
||||
}
|
||||
// From this point, we assume the uri is intended for a SPIFFE ID.
|
||||
if len(uri.String()) > 2048 {
|
||||
logger.Warning("invalid SPIFFE ID: total ID length larger than 2048 bytes")
|
||||
return nil
|
||||
}
|
||||
if len(uri.Host) == 0 || len(uri.Path) == 0 {
|
||||
logger.Warning("invalid SPIFFE ID: domain or workload ID is empty")
|
||||
return nil
|
||||
}
|
||||
if len(uri.Host) > 255 {
|
||||
logger.Warning("invalid SPIFFE ID: domain length larger than 255 characters")
|
||||
return nil
|
||||
}
|
||||
// A valid SPIFFE certificate can only have exactly one URI SAN field.
|
||||
if len(cert.URIs) > 1 {
|
||||
logger.Warning("invalid SPIFFE ID: multiple URI SANs")
|
||||
return nil
|
||||
}
|
||||
spiffeID = uri
|
||||
}
|
||||
return spiffeID
|
||||
}
|
||||
233
experimental/credentials/internal/spiffe_test.go
Normal file
233
experimental/credentials/internal/spiffe_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 internal
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/testdata"
|
||||
)
|
||||
|
||||
const wantURI = "spiffe://foo.bar.com/client/workload/1"
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func (s) TestSPIFFEIDFromState(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
urls []*url.URL
|
||||
// If we expect a SPIFFE ID to be returned.
|
||||
wantID bool
|
||||
}{
|
||||
{
|
||||
name: "empty URIs",
|
||||
urls: []*url.URL{},
|
||||
wantID: false,
|
||||
},
|
||||
{
|
||||
name: "good SPIFFE ID",
|
||||
urls: []*url.URL{
|
||||
{
|
||||
Scheme: "spiffe",
|
||||
Host: "foo.bar.com",
|
||||
Path: "workload/wl1",
|
||||
RawPath: "workload/wl1",
|
||||
},
|
||||
},
|
||||
wantID: true,
|
||||
},
|
||||
{
|
||||
name: "invalid host",
|
||||
urls: []*url.URL{
|
||||
{
|
||||
Scheme: "spiffe",
|
||||
Host: "",
|
||||
Path: "workload/wl1",
|
||||
RawPath: "workload/wl1",
|
||||
},
|
||||
},
|
||||
wantID: false,
|
||||
},
|
||||
{
|
||||
name: "invalid path",
|
||||
urls: []*url.URL{
|
||||
{
|
||||
Scheme: "spiffe",
|
||||
Host: "foo.bar.com",
|
||||
Path: "",
|
||||
RawPath: "",
|
||||
},
|
||||
},
|
||||
wantID: false,
|
||||
},
|
||||
{
|
||||
name: "large path",
|
||||
urls: []*url.URL{
|
||||
{
|
||||
Scheme: "spiffe",
|
||||
Host: "foo.bar.com",
|
||||
Path: string(make([]byte, 2050)),
|
||||
RawPath: string(make([]byte, 2050)),
|
||||
},
|
||||
},
|
||||
wantID: false,
|
||||
},
|
||||
{
|
||||
name: "large host",
|
||||
urls: []*url.URL{
|
||||
{
|
||||
Scheme: "spiffe",
|
||||
Host: string(make([]byte, 256)),
|
||||
Path: "workload/wl1",
|
||||
RawPath: "workload/wl1",
|
||||
},
|
||||
},
|
||||
wantID: false,
|
||||
},
|
||||
{
|
||||
name: "multiple URI SANs",
|
||||
urls: []*url.URL{
|
||||
{
|
||||
Scheme: "spiffe",
|
||||
Host: "foo.bar.com",
|
||||
Path: "workload/wl1",
|
||||
RawPath: "workload/wl1",
|
||||
},
|
||||
{
|
||||
Scheme: "spiffe",
|
||||
Host: "bar.baz.com",
|
||||
Path: "workload/wl2",
|
||||
RawPath: "workload/wl2",
|
||||
},
|
||||
{
|
||||
Scheme: "https",
|
||||
Host: "foo.bar.com",
|
||||
Path: "workload/wl1",
|
||||
RawPath: "workload/wl1",
|
||||
},
|
||||
},
|
||||
wantID: false,
|
||||
},
|
||||
{
|
||||
name: "multiple URI SANs without SPIFFE ID",
|
||||
urls: []*url.URL{
|
||||
{
|
||||
Scheme: "https",
|
||||
Host: "foo.bar.com",
|
||||
Path: "workload/wl1",
|
||||
RawPath: "workload/wl1",
|
||||
},
|
||||
{
|
||||
Scheme: "ssh",
|
||||
Host: "foo.bar.com",
|
||||
Path: "workload/wl1",
|
||||
RawPath: "workload/wl1",
|
||||
},
|
||||
},
|
||||
wantID: false,
|
||||
},
|
||||
{
|
||||
name: "multiple URI SANs with one SPIFFE ID",
|
||||
urls: []*url.URL{
|
||||
{
|
||||
Scheme: "spiffe",
|
||||
Host: "foo.bar.com",
|
||||
Path: "workload/wl1",
|
||||
RawPath: "workload/wl1",
|
||||
},
|
||||
{
|
||||
Scheme: "https",
|
||||
Host: "foo.bar.com",
|
||||
Path: "workload/wl1",
|
||||
RawPath: "workload/wl1",
|
||||
},
|
||||
},
|
||||
wantID: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state := tls.ConnectionState{PeerCertificates: []*x509.Certificate{{URIs: tt.urls}}}
|
||||
id := SPIFFEIDFromState(state)
|
||||
if got, want := id != nil, tt.wantID; got != want {
|
||||
t.Errorf("want wantID = %v, but SPIFFE ID is %v", want, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestSPIFFEIDFromCert(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataPath string
|
||||
// If we expect a SPIFFE ID to be returned.
|
||||
wantID bool
|
||||
}{
|
||||
{
|
||||
name: "good certificate with SPIFFE ID",
|
||||
dataPath: "x509/spiffe_cert.pem",
|
||||
wantID: true,
|
||||
},
|
||||
{
|
||||
name: "bad certificate with SPIFFE ID and another URI",
|
||||
dataPath: "x509/multiple_uri_cert.pem",
|
||||
wantID: false,
|
||||
},
|
||||
{
|
||||
name: "certificate without SPIFFE ID",
|
||||
dataPath: "x509/client1_cert.pem",
|
||||
wantID: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := os.ReadFile(testdata.Path(tt.dataPath))
|
||||
if err != nil {
|
||||
t.Fatalf("os.ReadFile(%s) failed: %v", testdata.Path(tt.dataPath), err)
|
||||
}
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
t.Fatalf("Failed to parse the certificate: byte block is nil")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("x509.ParseCertificate(%b) failed: %v", block.Bytes, err)
|
||||
}
|
||||
uri := SPIFFEIDFromCert(cert)
|
||||
if (uri != nil) != tt.wantID {
|
||||
t.Fatalf("wantID got and want mismatch, got %t, want %t", uri != nil, tt.wantID)
|
||||
}
|
||||
if uri != nil && uri.String() != wantURI {
|
||||
t.Fatalf("SPIFFE ID not expected, got %s, want %s", uri.String(), wantURI)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
58
experimental/credentials/internal/syscallconn.go
Normal file
58
experimental/credentials/internal/syscallconn.go
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 internal
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type sysConn = syscall.Conn
|
||||
|
||||
// syscallConn keeps reference of rawConn to support syscall.Conn for channelz.
|
||||
// SyscallConn() (the method in interface syscall.Conn) is explicitly
|
||||
// implemented on this type,
|
||||
//
|
||||
// Interface syscall.Conn is implemented by most net.Conn implementations (e.g.
|
||||
// TCPConn, UnixConn), but is not part of net.Conn interface. So wrapper conns
|
||||
// that embed net.Conn don't implement syscall.Conn. (Side note: tls.Conn
|
||||
// doesn't embed net.Conn, so even if syscall.Conn is part of net.Conn, it won't
|
||||
// help here).
|
||||
type syscallConn struct {
|
||||
net.Conn
|
||||
// sysConn is a type alias of syscall.Conn. It's necessary because the name
|
||||
// `Conn` collides with `net.Conn`.
|
||||
sysConn
|
||||
}
|
||||
|
||||
// WrapSyscallConn tries to wrap rawConn and newConn into a net.Conn that
|
||||
// implements syscall.Conn. rawConn will be used to support syscall, and newConn
|
||||
// will be used for read/write.
|
||||
//
|
||||
// This function returns newConn if rawConn doesn't implement syscall.Conn.
|
||||
func WrapSyscallConn(rawConn, newConn net.Conn) net.Conn {
|
||||
sysConn, ok := rawConn.(syscall.Conn)
|
||||
if !ok {
|
||||
return newConn
|
||||
}
|
||||
return &syscallConn{
|
||||
Conn: newConn,
|
||||
sysConn: sysConn,
|
||||
}
|
||||
}
|
||||
56
experimental/credentials/internal/syscallconn_test.go
Normal file
56
experimental/credentials/internal/syscallconn_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 internal
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (*syscallConn) SyscallConn() (syscall.RawConn, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type nonSyscallConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (s) TestWrapSyscallConn(t *testing.T) {
|
||||
sc := &syscallConn{}
|
||||
nsc := &nonSyscallConn{}
|
||||
|
||||
wrapConn := WrapSyscallConn(sc, nsc)
|
||||
if _, ok := wrapConn.(syscall.Conn); !ok {
|
||||
t.Errorf("returned conn (type %T) doesn't implement syscall.Conn, want implement", wrapConn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestWrapSyscallConnNoWrap(t *testing.T) {
|
||||
nscRaw := &nonSyscallConn{}
|
||||
nsc := &nonSyscallConn{}
|
||||
|
||||
wrapConn := WrapSyscallConn(nscRaw, nsc)
|
||||
if _, ok := wrapConn.(syscall.Conn); ok {
|
||||
t.Errorf("returned conn (type %T) implements syscall.Conn, want not implement", wrapConn)
|
||||
}
|
||||
if wrapConn != nsc {
|
||||
t.Errorf("returned conn is %p, want %p (the passed-in newConn)", wrapConn, nsc)
|
||||
}
|
||||
}
|
||||
249
experimental/credentials/tls.go
Normal file
249
experimental/credentials/tls.go
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 credentials provides experimental TLS credentials.
|
||||
// The use of this package is strongly discouraged. These credentials exist
|
||||
// solely to maintain compatibility for users interacting with clients that
|
||||
// violate the HTTP/2 specification by not advertising support for "h2" in ALPN.
|
||||
// This package is slated for removal in upcoming grpc-go releases. Users must
|
||||
// not rely on this package directly. Instead, they should either vendor a
|
||||
// specific version of gRPC or copy the relevant credentials into their own
|
||||
// codebase if absolutely necessary.
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/experimental/credentials/internal"
|
||||
)
|
||||
|
||||
// tlsCreds is the credentials required for authenticating a connection using TLS.
|
||||
type tlsCreds struct {
|
||||
// TLS configuration
|
||||
config *tls.Config
|
||||
}
|
||||
|
||||
func (c tlsCreds) Info() credentials.ProtocolInfo {
|
||||
return credentials.ProtocolInfo{
|
||||
SecurityProtocol: "tls",
|
||||
SecurityVersion: "1.2",
|
||||
ServerName: c.config.ServerName,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *tlsCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (_ net.Conn, _ credentials.AuthInfo, err error) {
|
||||
// use local cfg to avoid clobbering ServerName if using multiple endpoints
|
||||
cfg := cloneTLSConfig(c.config)
|
||||
|
||||
serverName, _, err := net.SplitHostPort(authority)
|
||||
if err != nil {
|
||||
// If the authority had no host port or if the authority cannot be parsed, use it as-is.
|
||||
serverName = authority
|
||||
}
|
||||
cfg.ServerName = serverName
|
||||
|
||||
conn := tls.Client(rawConn, cfg)
|
||||
errChannel := make(chan error, 1)
|
||||
go func() {
|
||||
errChannel <- conn.Handshake()
|
||||
close(errChannel)
|
||||
}()
|
||||
select {
|
||||
case err := <-errChannel:
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
conn.Close()
|
||||
return nil, nil, ctx.Err()
|
||||
}
|
||||
|
||||
tlsInfo := credentials.TLSInfo{
|
||||
State: conn.ConnectionState(),
|
||||
CommonAuthInfo: credentials.CommonAuthInfo{
|
||||
SecurityLevel: credentials.PrivacyAndIntegrity,
|
||||
},
|
||||
}
|
||||
id := internal.SPIFFEIDFromState(conn.ConnectionState())
|
||||
if id != nil {
|
||||
tlsInfo.SPIFFEID = id
|
||||
}
|
||||
return internal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
|
||||
}
|
||||
|
||||
func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
||||
conn := tls.Server(rawConn, c.config)
|
||||
if err := conn.Handshake(); err != nil {
|
||||
conn.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
cs := conn.ConnectionState()
|
||||
tlsInfo := credentials.TLSInfo{
|
||||
State: cs,
|
||||
CommonAuthInfo: credentials.CommonAuthInfo{
|
||||
SecurityLevel: credentials.PrivacyAndIntegrity,
|
||||
},
|
||||
}
|
||||
id := internal.SPIFFEIDFromState(conn.ConnectionState())
|
||||
if id != nil {
|
||||
tlsInfo.SPIFFEID = id
|
||||
}
|
||||
return internal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
|
||||
}
|
||||
|
||||
func (c *tlsCreds) Clone() credentials.TransportCredentials {
|
||||
return NewTLSWithALPNDisabled(c.config)
|
||||
}
|
||||
|
||||
func (c *tlsCreds) OverrideServerName(serverNameOverride string) error {
|
||||
c.config.ServerName = serverNameOverride
|
||||
return nil
|
||||
}
|
||||
|
||||
// The following cipher suites are forbidden for use with HTTP/2 by
|
||||
// https://datatracker.ietf.org/doc/html/rfc7540#appendix-A
|
||||
var tls12ForbiddenCipherSuites = map[uint16]struct{}{
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA: {},
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA: {},
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256: {},
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384: {},
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: {},
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: {},
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: {},
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: {},
|
||||
}
|
||||
|
||||
// NewTLSWithALPNDisabled uses c to construct a TransportCredentials based on
|
||||
// TLS. ALPN verification is disabled.
|
||||
func NewTLSWithALPNDisabled(c *tls.Config) credentials.TransportCredentials {
|
||||
config := applyDefaults(c)
|
||||
if config.GetConfigForClient != nil {
|
||||
oldFn := config.GetConfigForClient
|
||||
config.GetConfigForClient = func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
cfgForClient, err := oldFn(hello)
|
||||
if err != nil || cfgForClient == nil {
|
||||
return cfgForClient, err
|
||||
}
|
||||
return applyDefaults(cfgForClient), nil
|
||||
}
|
||||
}
|
||||
return &tlsCreds{config: config}
|
||||
}
|
||||
|
||||
func applyDefaults(c *tls.Config) *tls.Config {
|
||||
config := cloneTLSConfig(c)
|
||||
config.NextProtos = appendH2ToNextProtos(config.NextProtos)
|
||||
// If the user did not configure a MinVersion and did not configure a
|
||||
// MaxVersion < 1.2, use MinVersion=1.2, which is required by
|
||||
// https://datatracker.ietf.org/doc/html/rfc7540#section-9.2
|
||||
if config.MinVersion == 0 && (config.MaxVersion == 0 || config.MaxVersion >= tls.VersionTLS12) {
|
||||
config.MinVersion = tls.VersionTLS12
|
||||
}
|
||||
// If the user did not configure CipherSuites, use all "secure" cipher
|
||||
// suites reported by the TLS package, but remove some explicitly forbidden
|
||||
// by https://datatracker.ietf.org/doc/html/rfc7540#appendix-A
|
||||
if config.CipherSuites == nil {
|
||||
for _, cs := range tls.CipherSuites() {
|
||||
if _, ok := tls12ForbiddenCipherSuites[cs.ID]; !ok {
|
||||
config.CipherSuites = append(config.CipherSuites, cs.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// NewClientTLSFromCertWithALPNDisabled constructs TLS credentials from the
|
||||
// provided root certificate authority certificate(s) to validate server
|
||||
// connections. If certificates to establish the identity of the client need to
|
||||
// be included in the credentials (eg: for mTLS), use NewTLS instead, where a
|
||||
// complete tls.Config can be specified.
|
||||
// serverNameOverride is for testing only. If set to a non empty string,
|
||||
// it will override the virtual host name of authority (e.g. :authority header
|
||||
// field) in requests. ALPN verification is disabled.
|
||||
func NewClientTLSFromCertWithALPNDisabled(cp *x509.CertPool, serverNameOverride string) credentials.TransportCredentials {
|
||||
return NewTLSWithALPNDisabled(&tls.Config{ServerName: serverNameOverride, RootCAs: cp})
|
||||
}
|
||||
|
||||
// NewClientTLSFromFileWithALPNDisabled constructs TLS credentials from the
|
||||
// provided root certificate authority certificate file(s) to validate server
|
||||
// connections. If certificates to establish the identity of the client need to
|
||||
// be included in the credentials (eg: for mTLS), use NewTLS instead, where a
|
||||
// complete tls.Config can be specified.
|
||||
// serverNameOverride is for testing only. If set to a non empty string,
|
||||
// it will override the virtual host name of authority (e.g. :authority header
|
||||
// field) in requests. ALPN verification is disabled.
|
||||
func NewClientTLSFromFileWithALPNDisabled(certFile, serverNameOverride string) (credentials.TransportCredentials, error) {
|
||||
b, err := os.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cp := x509.NewCertPool()
|
||||
if !cp.AppendCertsFromPEM(b) {
|
||||
return nil, fmt.Errorf("credentials: failed to append certificates")
|
||||
}
|
||||
return NewTLSWithALPNDisabled(&tls.Config{ServerName: serverNameOverride, RootCAs: cp}), nil
|
||||
}
|
||||
|
||||
// NewServerTLSFromCertWithALPNDisabled constructs TLS credentials from the
|
||||
// input certificate for server. ALPN verification is disabled.
|
||||
func NewServerTLSFromCertWithALPNDisabled(cert *tls.Certificate) credentials.TransportCredentials {
|
||||
return NewTLSWithALPNDisabled(&tls.Config{Certificates: []tls.Certificate{*cert}})
|
||||
}
|
||||
|
||||
// NewServerTLSFromFileWithALPNDisabled constructs TLS credentials from the
|
||||
// input certificate file and key file for server. ALPN verification is disabled.
|
||||
func NewServerTLSFromFileWithALPNDisabled(certFile, keyFile string) (credentials.TransportCredentials, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewTLSWithALPNDisabled(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
|
||||
}
|
||||
|
||||
// cloneTLSConfig returns a shallow clone of the exported
|
||||
// fields of cfg, ignoring the unexported sync.Once, which
|
||||
// contains a mutex and must not be copied.
|
||||
//
|
||||
// If cfg is nil, a new zero tls.Config is returned.
|
||||
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
|
||||
return cfg.Clone()
|
||||
}
|
||||
|
||||
// appendH2ToNextProtos appends h2 to next protos.
|
||||
func appendH2ToNextProtos(ps []string) []string {
|
||||
for _, p := range ps {
|
||||
if p == http2.NextProtoTLS {
|
||||
return ps
|
||||
}
|
||||
}
|
||||
ret := make([]string, 0, len(ps)+1)
|
||||
ret = append(ret, ps...)
|
||||
return append(ret, http2.NextProtoTLS)
|
||||
}
|
||||
604
experimental/credentials/tls_ext_test.go
Normal file
604
experimental/credentials/tls_ext_test.go
Normal file
@@ -0,0 +1,604 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 credentials_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
credsstable "google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/experimental/credentials"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/testdata"
|
||||
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
const defaultTestTimeout = 10 * time.Second
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
var serverCert tls.Certificate
|
||||
var certPool *x509.CertPool
|
||||
var serverName = "x.test.example.com"
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
serverCert, err = tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("tls.LoadX509KeyPair(server1.pem, server1.key) failed: %v", err))
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(testdata.Path("x509/server_ca_cert.pem"))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error reading CA cert file: %v", err))
|
||||
}
|
||||
certPool = x509.NewCertPool()
|
||||
if !certPool.AppendCertsFromPEM(b) {
|
||||
panic("Error appending cert from PEM")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that the MinVersion of tls.Config is set to 1.2 if it is not already
|
||||
// set by the user.
|
||||
func (s) TestTLS_MinVersion12(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
serverTLS func() *tls.Config
|
||||
}{
|
||||
{
|
||||
name: "base_case",
|
||||
serverTLS: func() *tls.Config {
|
||||
return &tls.Config{
|
||||
// MinVersion should be set to 1.2 by gRPC by default.
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fallback_to_base",
|
||||
serverTLS: func() *tls.Config {
|
||||
config := &tls.Config{
|
||||
// MinVersion should be set to 1.2 by gRPC by default.
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
}
|
||||
config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return nil, nil
|
||||
}
|
||||
return config
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dynamic_using_get_config_for_client",
|
||||
serverTLS: func() *tls.Config {
|
||||
return &tls.Config{
|
||||
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return &tls.Config{
|
||||
// MinVersion should be set to 1.2 by gRPC by default.
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create server creds without a minimum version.
|
||||
serverCreds := credentials.NewTLSWithALPNDisabled(tc.serverTLS())
|
||||
ss := stubserver.StubServer{
|
||||
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
|
||||
return &testpb.Empty{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create client creds that supports V1.0-V1.1.
|
||||
clientCreds := credentials.NewTLSWithALPNDisabled(&tls.Config{
|
||||
ServerName: serverName,
|
||||
RootCAs: certPool,
|
||||
MinVersion: tls.VersionTLS10,
|
||||
MaxVersion: tls.VersionTLS11,
|
||||
})
|
||||
|
||||
// Start server and client separately, because Start() blocks on a
|
||||
// successful connection, which we will not get.
|
||||
if err := ss.StartServer(grpc.Creds(serverCreds)); err != nil {
|
||||
t.Fatalf("Error starting server: %v", err)
|
||||
}
|
||||
defer ss.Stop()
|
||||
|
||||
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient error: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
|
||||
const wantStr = "authentication handshake failed"
|
||||
if _, err = client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable || !strings.Contains(status.Convert(err).Message(), wantStr) {
|
||||
t.Fatalf("EmptyCall err = %v; want code=%v, message contains %q", err, codes.Unavailable, wantStr)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that the MinVersion of tls.Config is not changed if it is set by the
|
||||
// user.
|
||||
func (s) TestTLS_MinVersionOverridable(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
var allCipherSuites []uint16
|
||||
for _, cs := range tls.CipherSuites() {
|
||||
allCipherSuites = append(allCipherSuites, cs.ID)
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
serverTLS func() *tls.Config
|
||||
}{
|
||||
{
|
||||
name: "base_case",
|
||||
serverTLS: func() *tls.Config {
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS10,
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
CipherSuites: allCipherSuites,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fallback_to_base",
|
||||
serverTLS: func() *tls.Config {
|
||||
config := &tls.Config{
|
||||
MinVersion: tls.VersionTLS10,
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
CipherSuites: allCipherSuites,
|
||||
}
|
||||
config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return nil, nil
|
||||
}
|
||||
return config
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dynamic_using_get_config_for_client",
|
||||
serverTLS: func() *tls.Config {
|
||||
return &tls.Config{
|
||||
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS10,
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
CipherSuites: allCipherSuites,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create server creds that allow v1.0.
|
||||
serverCreds := credentials.NewTLSWithALPNDisabled(tc.serverTLS())
|
||||
ss := stubserver.StubServer{
|
||||
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
|
||||
return &testpb.Empty{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create client creds that supports V1.0-V1.1.
|
||||
clientCreds := credentials.NewTLSWithALPNDisabled(&tls.Config{
|
||||
ServerName: serverName,
|
||||
RootCAs: certPool,
|
||||
CipherSuites: allCipherSuites,
|
||||
MinVersion: tls.VersionTLS10,
|
||||
MaxVersion: tls.VersionTLS11,
|
||||
})
|
||||
|
||||
if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
|
||||
t.Fatalf("Error starting stub server: %v", err)
|
||||
}
|
||||
defer ss.Stop()
|
||||
|
||||
if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall err = %v; want <nil>", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that CipherSuites is set to exclude HTTP/2 forbidden suites by default.
|
||||
func (s) TestTLS_CipherSuites(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
serverTLS func() *tls.Config
|
||||
}{
|
||||
{
|
||||
name: "base_case",
|
||||
serverTLS: func() *tls.Config {
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fallback_to_base",
|
||||
serverTLS: func() *tls.Config {
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
}
|
||||
config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return nil, nil
|
||||
}
|
||||
return config
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dynamic_using_get_config_for_client",
|
||||
serverTLS: func() *tls.Config {
|
||||
return &tls.Config{
|
||||
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create server creds without cipher suites.
|
||||
serverCreds := credentials.NewTLSWithALPNDisabled(tc.serverTLS())
|
||||
ss := stubserver.StubServer{
|
||||
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
|
||||
return &testpb.Empty{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create client creds that use a forbidden suite only.
|
||||
clientCreds := credentials.NewTLSWithALPNDisabled(&tls.Config{
|
||||
ServerName: serverName,
|
||||
RootCAs: certPool,
|
||||
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
|
||||
MaxVersion: tls.VersionTLS12, // TLS1.3 cipher suites are not configurable, so limit to 1.2.
|
||||
})
|
||||
|
||||
// Start server and client separately, because Start() blocks on a
|
||||
// successful connection, which we will not get.
|
||||
if err := ss.StartServer(grpc.Creds(serverCreds)); err != nil {
|
||||
t.Fatalf("Error starting server: %v", err)
|
||||
}
|
||||
defer ss.Stop()
|
||||
|
||||
cc, err := grpc.NewClient("dns:"+ss.Address, grpc.WithTransportCredentials(clientCreds))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient error: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
|
||||
const wantStr = "authentication handshake failed"
|
||||
if _, err = client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable || !strings.Contains(status.Convert(err).Message(), wantStr) {
|
||||
t.Fatalf("EmptyCall err = %v; want code=%v, message contains %q", err, codes.Unavailable, wantStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that CipherSuites is not overridden when it is set.
|
||||
func (s) TestTLS_CipherSuitesOverridable(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
serverTLS func() *tls.Config
|
||||
}{
|
||||
{
|
||||
name: "base_case",
|
||||
serverTLS: func() *tls.Config {
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fallback_to_base",
|
||||
serverTLS: func() *tls.Config {
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
|
||||
}
|
||||
config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return nil, nil
|
||||
}
|
||||
return config
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dynamic_using_get_config_for_client",
|
||||
serverTLS: func() *tls.Config {
|
||||
return &tls.Config{
|
||||
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create server that allows only a forbidden cipher suite.
|
||||
serverCreds := credentials.NewTLSWithALPNDisabled(tc.serverTLS())
|
||||
ss := stubserver.StubServer{
|
||||
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
|
||||
return &testpb.Empty{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create server that allows only a forbidden cipher suite.
|
||||
clientCreds := credentials.NewTLSWithALPNDisabled(&tls.Config{
|
||||
ServerName: serverName,
|
||||
RootCAs: certPool,
|
||||
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
|
||||
MaxVersion: tls.VersionTLS12, // TLS1.3 cipher suites are not configurable, so limit to 1.2.
|
||||
})
|
||||
|
||||
if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
|
||||
t.Fatalf("Error starting stub server: %v", err)
|
||||
}
|
||||
defer ss.Stop()
|
||||
|
||||
if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall err = %v; want <nil>", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTLS_ServerConfiguresALPNByDefault verifies that ALPN is configured
|
||||
// correctly for a server that doesn't specify the NextProtos field and uses
|
||||
// GetConfigForClient to provide the TLS config during the handshake.
|
||||
func (s) TestTLS_ServerConfiguresALPNByDefault(t *testing.T) {
|
||||
initialVal := envconfig.EnforceALPNEnabled
|
||||
defer func() {
|
||||
envconfig.EnforceALPNEnabled = initialVal
|
||||
}()
|
||||
envconfig.EnforceALPNEnabled = true
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Create a server that doesn't set the NextProtos field.
|
||||
serverCreds := credentials.NewTLSWithALPNDisabled(&tls.Config{
|
||||
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
ss := stubserver.StubServer{
|
||||
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
|
||||
return &testpb.Empty{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
clientCreds := credsstable.NewTLS(&tls.Config{
|
||||
ServerName: serverName,
|
||||
RootCAs: certPool,
|
||||
})
|
||||
|
||||
if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
|
||||
t.Fatalf("Error starting stub server: %v", err)
|
||||
}
|
||||
defer ss.Stop()
|
||||
|
||||
if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall err = %v; want <nil>", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTLS_DisabledALPNClient tests the behaviour of TransportCredentials when
|
||||
// connecting to a server that doesn't support ALPN.
|
||||
func (s) TestTLS_DisabledALPNClient(t *testing.T) {
|
||||
initialVal := envconfig.EnforceALPNEnabled
|
||||
defer func() {
|
||||
envconfig.EnforceALPNEnabled = initialVal
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
alpnEnforced bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "enforced",
|
||||
},
|
||||
{
|
||||
name: "not_enforced",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
envconfig.EnforceALPNEnabled = tc.alpnEnforced
|
||||
|
||||
listener, err := tls.Listen("tcp", "localhost:0", &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
NextProtos: []string{}, // Empty list indicates ALPN is disabled.
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting TLS server: %v", err)
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("listener.Accept returned error: %v", err)
|
||||
} else {
|
||||
// The first write to the TLS listener initiates the TLS handshake.
|
||||
conn.Write([]byte("Hello, World!"))
|
||||
conn.Close()
|
||||
}
|
||||
close(errCh)
|
||||
}()
|
||||
|
||||
serverAddr := listener.Addr().String()
|
||||
conn, err := net.Dial("tcp", serverAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("net.Dial(%s) failed: %v", serverAddr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
clientCfg := tls.Config{
|
||||
ServerName: serverName,
|
||||
RootCAs: certPool,
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
_, _, err = credentials.NewTLSWithALPNDisabled(&clientCfg).ClientHandshake(ctx, serverName, conn)
|
||||
|
||||
if gotErr := (err != nil); gotErr != tc.wantErr {
|
||||
t.Errorf("ClientHandshake returned unexpected error: got=%v, want=%t", err, tc.wantErr)
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error received from server: %v", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout waiting for error from server")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTLS_DisabledALPNServer tests the behaviour of TransportCredentials when
|
||||
// accepting a request from a client that doesn't support ALPN.
|
||||
func (s) TestTLS_DisabledALPNServer(t *testing.T) {
|
||||
initialVal := envconfig.EnforceALPNEnabled
|
||||
defer func() {
|
||||
envconfig.EnforceALPNEnabled = initialVal
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
alpnEnforced bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "enforced",
|
||||
},
|
||||
{
|
||||
name: "not_enforced",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
envconfig.EnforceALPNEnabled = tc.alpnEnforced
|
||||
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting server: %v", err)
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("listener.Accept returned error: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
serverCfg := tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
_, _, err = credentials.NewTLSWithALPNDisabled(&serverCfg).ServerHandshake(conn)
|
||||
if gotErr := (err != nil); gotErr != tc.wantErr {
|
||||
t.Errorf("ServerHandshake returned unexpected error: got=%v, want=%t", err, tc.wantErr)
|
||||
}
|
||||
close(errCh)
|
||||
}()
|
||||
|
||||
serverAddr := listener.Addr().String()
|
||||
clientCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
NextProtos: []string{}, // Empty list indicates ALPN is disabled.
|
||||
RootCAs: certPool,
|
||||
ServerName: serverName,
|
||||
}
|
||||
conn, err := tls.Dial("tcp", serverAddr, clientCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("tls.Dial(%s) failed: %v", serverAddr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
select {
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Fatal("Timed out waiting for completion")
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected server error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
85
experimental/experimental.go
Normal file
85
experimental/experimental.go
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2023 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 experimental is a collection of experimental features that might
|
||||
// have some rough edges to them. Housing experimental features in this package
|
||||
// results in a user accessing these APIs as `experimental.Foo`, thereby making
|
||||
// it explicit that the feature is experimental and using them in production
|
||||
// code is at their own risk.
|
||||
//
|
||||
// All APIs in this package are experimental.
|
||||
package experimental
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/mem"
|
||||
)
|
||||
|
||||
// SetDefaultBufferPool sets the default buffer pool used by all grpc clients
|
||||
// and servers that do not have a buffer pool configured via WithBufferPool or
|
||||
// BufferPool. It also changes the buffer pool used by the proto codec, which
|
||||
// can't be changed otherwise. The provided buffer pool must be non-nil. The
|
||||
// default value is mem.DefaultBufferPool.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. The last caller wins.
|
||||
func SetDefaultBufferPool(bufferPool mem.BufferPool) {
|
||||
internal.SetDefaultBufferPool.(func(mem.BufferPool))(bufferPool)
|
||||
}
|
||||
|
||||
// WithBufferPool returns a grpc.DialOption that configures the use of bufferPool
|
||||
// for parsing incoming messages on a grpc.ClientConn, and for temporary buffers
|
||||
// when marshaling outgoing messages. By default, mem.DefaultBufferPool is used,
|
||||
// and this option only exists to provide alternative buffer pool implementations
|
||||
// to the client, such as more optimized size allocations etc. However, the
|
||||
// default buffer pool is already tuned to account for many different use-cases.
|
||||
//
|
||||
// Note: The following options will interfere with the buffer pool because they
|
||||
// require a fully materialized buffer instead of a sequence of buffers:
|
||||
// EnableTracing, and binary logging. In such cases, materializing the buffer
|
||||
// will generate a lot of garbage, reducing the overall benefit from using a
|
||||
// pool.
|
||||
func WithBufferPool(bufferPool mem.BufferPool) grpc.DialOption {
|
||||
return internal.WithBufferPool.(func(mem.BufferPool) grpc.DialOption)(bufferPool)
|
||||
}
|
||||
|
||||
// BufferPool returns a grpc.ServerOption that configures the server to use the
|
||||
// provided buffer pool for parsing incoming messages and for temporary buffers
|
||||
// when marshaling outgoing messages. By default, mem.DefaultBufferPool is used,
|
||||
// and this option only exists to provide alternative buffer pool implementations
|
||||
// to the server, such as more optimized size allocations etc. However, the
|
||||
// default buffer pool is already tuned to account for many different use-cases.
|
||||
//
|
||||
// Note: The following options will interfere with the buffer pool because they
|
||||
// require a fully materialized buffer instead of a sequence of buffers:
|
||||
// EnableTracing, and binary logging. In such cases, materializing the buffer
|
||||
// will generate a lot of garbage, reducing the overall benefit from using a
|
||||
// pool.
|
||||
func BufferPool(bufferPool mem.BufferPool) grpc.ServerOption {
|
||||
return internal.BufferPool.(func(mem.BufferPool) grpc.ServerOption)(bufferPool)
|
||||
}
|
||||
|
||||
// AcceptCompressors returns a CallOption that limits the values
|
||||
// advertised in the grpc-accept-encoding header for the provided RPC. The
|
||||
// supplied names must correspond to compressors registered via
|
||||
// encoding.RegisterCompressor. Passing no names advertises "identity" (no
|
||||
// compression) only.
|
||||
func AcceptCompressors(names ...string) grpc.CallOption {
|
||||
return internal.AcceptCompressors.(func(...string) grpc.CallOption)(names...)
|
||||
}
|
||||
36
experimental/opentelemetry/trace_options.go
Normal file
36
experimental/opentelemetry/trace_options.go
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2024 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 opentelemetry is EXPERIMENTAL and will be moved to stats/opentelemetry
|
||||
// package in a later release.
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// TraceOptions contains the tracing settings for OpenTelemetry instrumentation.
|
||||
type TraceOptions struct {
|
||||
// TracerProvider is the OpenTelemetry tracer which is required to
|
||||
// record traces/trace spans for instrumentation. If unset, tracing
|
||||
// will not be recorded.
|
||||
TracerProvider trace.TracerProvider
|
||||
|
||||
// TextMapPropagator propagates span context through text map carrier.
|
||||
// If unset, tracing will not be recorded.
|
||||
TextMapPropagator propagation.TextMapPropagator
|
||||
}
|
||||
211
experimental/shared_buffer_pool_test.go
Normal file
211
experimental/shared_buffer_pool_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2023 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 experimental_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/encoding/gzip"
|
||||
"google.golang.org/grpc/experimental"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
const defaultTestTimeout = 10 * time.Second
|
||||
|
||||
func (s) TestRecvBufferPoolStream(t *testing.T) {
|
||||
// TODO: How much of this test can be preserved now that buffer reuse happens at
|
||||
// the codec and HTTP/2 level?
|
||||
t.SkipNow()
|
||||
tcs := []struct {
|
||||
name string
|
||||
callOpts []grpc.CallOption
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
},
|
||||
{
|
||||
name: "useCompressor",
|
||||
callOpts: []grpc.CallOption{
|
||||
grpc.UseCompressor(gzip.Name),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
const reqCount = 10
|
||||
|
||||
ss := &stubserver.StubServer{
|
||||
FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error {
|
||||
for i := 0; i < reqCount; i++ {
|
||||
preparedMsg := &grpc.PreparedMsg{}
|
||||
if err := preparedMsg.Encode(stream, &testpb.StreamingOutputCallResponse{
|
||||
Payload: &testpb.Payload{
|
||||
Body: []byte{'0' + uint8(i)},
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
stream.SendMsg(preparedMsg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
pool := &checkBufferPool{}
|
||||
sopts := []grpc.ServerOption{experimental.BufferPool(pool)}
|
||||
dopts := []grpc.DialOption{experimental.WithBufferPool(pool)}
|
||||
if err := ss.Start(sopts, dopts...); err != nil {
|
||||
t.Fatalf("Error starting endpoint server: %v", err)
|
||||
}
|
||||
defer ss.Stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
stream, err := ss.Client.FullDuplexCall(ctx, tc.callOpts...)
|
||||
if err != nil {
|
||||
t.Fatalf("ss.Client.FullDuplexCall failed: %v", err)
|
||||
}
|
||||
|
||||
var ngot int
|
||||
var buf bytes.Buffer
|
||||
for {
|
||||
reply, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ngot++
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
buf.Write(reply.GetPayload().GetBody())
|
||||
}
|
||||
if want := 10; ngot != want {
|
||||
t.Fatalf("Got %d replies, want %d", ngot, want)
|
||||
}
|
||||
if got, want := buf.String(), "0,1,2,3,4,5,6,7,8,9"; got != want {
|
||||
t.Fatalf("Got replies %q; want %q", got, want)
|
||||
}
|
||||
|
||||
if len(pool.puts) != reqCount {
|
||||
t.Fatalf("Expected 10 buffers to be returned to the pool, got %d", len(pool.puts))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestRecvBufferPoolUnary(t *testing.T) {
|
||||
// TODO: See above
|
||||
t.SkipNow()
|
||||
tcs := []struct {
|
||||
name string
|
||||
callOpts []grpc.CallOption
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
},
|
||||
{
|
||||
name: "useCompressor",
|
||||
callOpts: []grpc.CallOption{
|
||||
grpc.UseCompressor(gzip.Name),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
const largeSize = 1024
|
||||
|
||||
ss := &stubserver.StubServer{
|
||||
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
|
||||
return &testpb.SimpleResponse{
|
||||
Payload: &testpb.Payload{
|
||||
Body: make([]byte, largeSize),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pool := &checkBufferPool{}
|
||||
sopts := []grpc.ServerOption{experimental.BufferPool(pool)}
|
||||
dopts := []grpc.DialOption{experimental.WithBufferPool(pool)}
|
||||
if err := ss.Start(sopts, dopts...); err != nil {
|
||||
t.Fatalf("Error starting endpoint server: %v", err)
|
||||
}
|
||||
defer ss.Stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
const reqCount = 10
|
||||
for i := 0; i < reqCount; i++ {
|
||||
if _, err := ss.Client.UnaryCall(
|
||||
ctx,
|
||||
&testpb.SimpleRequest{
|
||||
Payload: &testpb.Payload{
|
||||
Body: make([]byte, largeSize),
|
||||
},
|
||||
},
|
||||
tc.callOpts...,
|
||||
); err != nil {
|
||||
t.Fatalf("ss.Client.UnaryCall failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
const bufferCount = reqCount * 2 // req + resp
|
||||
if len(pool.puts) != bufferCount {
|
||||
t.Fatalf("Expected %d buffers to be returned to the pool, got %d", bufferCount, len(pool.puts))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type checkBufferPool struct {
|
||||
puts [][]byte
|
||||
}
|
||||
|
||||
func (p *checkBufferPool) Get(size int) *[]byte {
|
||||
b := make([]byte, size)
|
||||
return &b
|
||||
}
|
||||
|
||||
func (p *checkBufferPool) Put(bs *[]byte) {
|
||||
p.puts = append(p.puts, *bs)
|
||||
}
|
||||
342
experimental/stats/metricregistry.go
Normal file
342
experimental/stats/metricregistry.go
Normal file
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 stats
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/stats"
|
||||
)
|
||||
|
||||
func init() {
|
||||
internal.SnapshotMetricRegistryForTesting = snapshotMetricsRegistryForTesting
|
||||
}
|
||||
|
||||
var logger = grpclog.Component("metrics-registry")
|
||||
|
||||
// DefaultMetrics are the default metrics registered through global metrics
|
||||
// registry. This is written to at initialization time only, and is read only
|
||||
// after initialization.
|
||||
var DefaultMetrics = stats.NewMetricSet()
|
||||
|
||||
// MetricDescriptor is the data for a registered metric.
|
||||
type MetricDescriptor struct {
|
||||
// The name of this metric. This name must be unique across the whole binary
|
||||
// (including any per call metrics). See
|
||||
// https://github.com/grpc/proposal/blob/master/A79-non-per-call-metrics-architecture.md#metric-instrument-naming-conventions
|
||||
// for metric naming conventions.
|
||||
Name string
|
||||
// The description of this metric.
|
||||
Description string
|
||||
// The unit (e.g. entries, seconds) of this metric.
|
||||
Unit string
|
||||
// The required label keys for this metric. These are intended to
|
||||
// metrics emitted from a stats handler.
|
||||
Labels []string
|
||||
// The optional label keys for this metric. These are intended to attached
|
||||
// to metrics emitted from a stats handler if configured.
|
||||
OptionalLabels []string
|
||||
// Whether this metric is on by default.
|
||||
Default bool
|
||||
// The type of metric. This is set by the metric registry, and not intended
|
||||
// to be set by a component registering a metric.
|
||||
Type MetricType
|
||||
// Bounds are the bounds of this metric. This only applies to histogram
|
||||
// metrics. If unset or set with length 0, stats handlers will fall back to
|
||||
// default bounds.
|
||||
Bounds []float64
|
||||
}
|
||||
|
||||
// MetricType is the type of metric.
|
||||
type MetricType int
|
||||
|
||||
// Type of metric supported by this instrument registry.
|
||||
const (
|
||||
MetricTypeIntCount MetricType = iota
|
||||
MetricTypeFloatCount
|
||||
MetricTypeIntHisto
|
||||
MetricTypeFloatHisto
|
||||
MetricTypeIntGauge
|
||||
MetricTypeIntUpDownCount
|
||||
MetricTypeIntAsyncGauge
|
||||
)
|
||||
|
||||
// Int64CountHandle is a typed handle for a int count metric. This handle
|
||||
// is passed at the recording point in order to know which metric to record
|
||||
// on.
|
||||
type Int64CountHandle MetricDescriptor
|
||||
|
||||
// Descriptor returns the int64 count handle typecast to a pointer to a
|
||||
// MetricDescriptor.
|
||||
func (h *Int64CountHandle) Descriptor() *MetricDescriptor {
|
||||
return (*MetricDescriptor)(h)
|
||||
}
|
||||
|
||||
// Record records the int64 count value on the metrics recorder provided.
|
||||
func (h *Int64CountHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) {
|
||||
recorder.RecordInt64Count(h, incr, labels...)
|
||||
}
|
||||
|
||||
// Int64UpDownCountHandle is a typed handle for an int up-down counter metric.
|
||||
// This handle is passed at the recording point in order to know which metric
|
||||
// to record on.
|
||||
type Int64UpDownCountHandle MetricDescriptor
|
||||
|
||||
// Descriptor returns the int64 up-down counter handle typecast to a pointer to a
|
||||
// MetricDescriptor.
|
||||
func (h *Int64UpDownCountHandle) Descriptor() *MetricDescriptor {
|
||||
return (*MetricDescriptor)(h)
|
||||
}
|
||||
|
||||
// Record records the int64 up-down counter value on the metrics recorder provided.
|
||||
// The value 'v' can be positive to increment or negative to decrement.
|
||||
func (h *Int64UpDownCountHandle) Record(recorder MetricsRecorder, v int64, labels ...string) {
|
||||
recorder.RecordInt64UpDownCount(h, v, labels...)
|
||||
}
|
||||
|
||||
// Float64CountHandle is a typed handle for a float count metric. This handle is
|
||||
// passed at the recording point in order to know which metric to record on.
|
||||
type Float64CountHandle MetricDescriptor
|
||||
|
||||
// Descriptor returns the float64 count handle typecast to a pointer to a
|
||||
// MetricDescriptor.
|
||||
func (h *Float64CountHandle) Descriptor() *MetricDescriptor {
|
||||
return (*MetricDescriptor)(h)
|
||||
}
|
||||
|
||||
// Record records the float64 count value on the metrics recorder provided.
|
||||
func (h *Float64CountHandle) Record(recorder MetricsRecorder, incr float64, labels ...string) {
|
||||
recorder.RecordFloat64Count(h, incr, labels...)
|
||||
}
|
||||
|
||||
// Int64HistoHandle is a typed handle for an int histogram metric. This handle
|
||||
// is passed at the recording point in order to know which metric to record on.
|
||||
type Int64HistoHandle MetricDescriptor
|
||||
|
||||
// Descriptor returns the int64 histo handle typecast to a pointer to a
|
||||
// MetricDescriptor.
|
||||
func (h *Int64HistoHandle) Descriptor() *MetricDescriptor {
|
||||
return (*MetricDescriptor)(h)
|
||||
}
|
||||
|
||||
// Record records the int64 histo value on the metrics recorder provided.
|
||||
func (h *Int64HistoHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) {
|
||||
recorder.RecordInt64Histo(h, incr, labels...)
|
||||
}
|
||||
|
||||
// Float64HistoHandle is a typed handle for a float histogram metric. This
|
||||
// handle is passed at the recording point in order to know which metric to
|
||||
// record on.
|
||||
type Float64HistoHandle MetricDescriptor
|
||||
|
||||
// Descriptor returns the float64 histo handle typecast to a pointer to a
|
||||
// MetricDescriptor.
|
||||
func (h *Float64HistoHandle) Descriptor() *MetricDescriptor {
|
||||
return (*MetricDescriptor)(h)
|
||||
}
|
||||
|
||||
// Record records the float64 histo value on the metrics recorder provided.
|
||||
func (h *Float64HistoHandle) Record(recorder MetricsRecorder, incr float64, labels ...string) {
|
||||
recorder.RecordFloat64Histo(h, incr, labels...)
|
||||
}
|
||||
|
||||
// Int64GaugeHandle is a typed handle for an int gauge metric. This handle is
|
||||
// passed at the recording point in order to know which metric to record on.
|
||||
type Int64GaugeHandle MetricDescriptor
|
||||
|
||||
// Descriptor returns the int64 gauge handle typecast to a pointer to a
|
||||
// MetricDescriptor.
|
||||
func (h *Int64GaugeHandle) Descriptor() *MetricDescriptor {
|
||||
return (*MetricDescriptor)(h)
|
||||
}
|
||||
|
||||
// Record records the int64 histo value on the metrics recorder provided.
|
||||
func (h *Int64GaugeHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) {
|
||||
recorder.RecordInt64Gauge(h, incr, labels...)
|
||||
}
|
||||
|
||||
// AsyncMetric is a marker interface for asynchronous metric types.
|
||||
type AsyncMetric interface {
|
||||
isAsync()
|
||||
Descriptor() *MetricDescriptor
|
||||
}
|
||||
|
||||
// Int64AsyncGaugeHandle is a typed handle for an int gauge metric. This handle is
|
||||
// passed at the recording point in order to know which metric to record on.
|
||||
type Int64AsyncGaugeHandle MetricDescriptor
|
||||
|
||||
// isAsync implements the AsyncMetric interface.
|
||||
func (h *Int64AsyncGaugeHandle) isAsync() {}
|
||||
|
||||
// Descriptor returns the int64 gauge handle typecast to a pointer to a
|
||||
// MetricDescriptor.
|
||||
func (h *Int64AsyncGaugeHandle) Descriptor() *MetricDescriptor {
|
||||
return (*MetricDescriptor)(h)
|
||||
}
|
||||
|
||||
// Record records the int64 gauge value on the metrics recorder provided.
|
||||
func (h *Int64AsyncGaugeHandle) Record(recorder AsyncMetricsRecorder, value int64, labels ...string) {
|
||||
recorder.RecordInt64AsyncGauge(h, value, labels...)
|
||||
}
|
||||
|
||||
// registeredMetrics are the registered metric descriptor names.
|
||||
var registeredMetrics = make(map[string]bool)
|
||||
|
||||
// metricsRegistry contains all of the registered metrics.
|
||||
//
|
||||
// This is written to only at init time, and read only after that.
|
||||
var metricsRegistry = make(map[string]*MetricDescriptor)
|
||||
|
||||
// DescriptorForMetric returns the MetricDescriptor from the global registry.
|
||||
//
|
||||
// Returns nil if MetricDescriptor not present.
|
||||
func DescriptorForMetric(metricName string) *MetricDescriptor {
|
||||
return metricsRegistry[metricName]
|
||||
}
|
||||
|
||||
func registerMetric(metricName string, def bool) {
|
||||
if registeredMetrics[metricName] {
|
||||
logger.Fatalf("metric %v already registered", metricName)
|
||||
}
|
||||
registeredMetrics[metricName] = true
|
||||
if def {
|
||||
DefaultMetrics = DefaultMetrics.Add(metricName)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterInt64Count registers the metric description onto the global registry.
|
||||
// It returns a typed handle to use to recording data.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. If multiple metrics are
|
||||
// registered with the same name, this function will panic.
|
||||
func RegisterInt64Count(descriptor MetricDescriptor) *Int64CountHandle {
|
||||
registerMetric(descriptor.Name, descriptor.Default)
|
||||
descriptor.Type = MetricTypeIntCount
|
||||
descPtr := &descriptor
|
||||
metricsRegistry[descriptor.Name] = descPtr
|
||||
return (*Int64CountHandle)(descPtr)
|
||||
}
|
||||
|
||||
// RegisterFloat64Count registers the metric description onto the global
|
||||
// registry. It returns a typed handle to use to recording data.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. If multiple metrics are
|
||||
// registered with the same name, this function will panic.
|
||||
func RegisterFloat64Count(descriptor MetricDescriptor) *Float64CountHandle {
|
||||
registerMetric(descriptor.Name, descriptor.Default)
|
||||
descriptor.Type = MetricTypeFloatCount
|
||||
descPtr := &descriptor
|
||||
metricsRegistry[descriptor.Name] = descPtr
|
||||
return (*Float64CountHandle)(descPtr)
|
||||
}
|
||||
|
||||
// RegisterInt64Histo registers the metric description onto the global registry.
|
||||
// It returns a typed handle to use to recording data.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. If multiple metrics are
|
||||
// registered with the same name, this function will panic.
|
||||
func RegisterInt64Histo(descriptor MetricDescriptor) *Int64HistoHandle {
|
||||
registerMetric(descriptor.Name, descriptor.Default)
|
||||
descriptor.Type = MetricTypeIntHisto
|
||||
descPtr := &descriptor
|
||||
metricsRegistry[descriptor.Name] = descPtr
|
||||
return (*Int64HistoHandle)(descPtr)
|
||||
}
|
||||
|
||||
// RegisterFloat64Histo registers the metric description onto the global
|
||||
// registry. It returns a typed handle to use to recording data.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. If multiple metrics are
|
||||
// registered with the same name, this function will panic.
|
||||
func RegisterFloat64Histo(descriptor MetricDescriptor) *Float64HistoHandle {
|
||||
registerMetric(descriptor.Name, descriptor.Default)
|
||||
descriptor.Type = MetricTypeFloatHisto
|
||||
descPtr := &descriptor
|
||||
metricsRegistry[descriptor.Name] = descPtr
|
||||
return (*Float64HistoHandle)(descPtr)
|
||||
}
|
||||
|
||||
// RegisterInt64Gauge registers the metric description onto the global registry.
|
||||
// It returns a typed handle to use to recording data.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. If multiple metrics are
|
||||
// registered with the same name, this function will panic.
|
||||
func RegisterInt64Gauge(descriptor MetricDescriptor) *Int64GaugeHandle {
|
||||
registerMetric(descriptor.Name, descriptor.Default)
|
||||
descriptor.Type = MetricTypeIntGauge
|
||||
descPtr := &descriptor
|
||||
metricsRegistry[descriptor.Name] = descPtr
|
||||
return (*Int64GaugeHandle)(descPtr)
|
||||
}
|
||||
|
||||
// RegisterInt64UpDownCount registers the metric description onto the global registry.
|
||||
// It returns a typed handle to use for recording data.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. If multiple metrics are
|
||||
// registered with the same name, this function will panic.
|
||||
func RegisterInt64UpDownCount(descriptor MetricDescriptor) *Int64UpDownCountHandle {
|
||||
registerMetric(descriptor.Name, descriptor.Default)
|
||||
// Set the specific metric type for the up-down counter
|
||||
descriptor.Type = MetricTypeIntUpDownCount
|
||||
descPtr := &descriptor
|
||||
metricsRegistry[descriptor.Name] = descPtr
|
||||
return (*Int64UpDownCountHandle)(descPtr)
|
||||
}
|
||||
|
||||
// RegisterInt64AsyncGauge registers the metric description onto the global registry.
|
||||
// It returns a typed handle to use for recording data.
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. If multiple metrics are
|
||||
// registered with the same name, this function will panic.
|
||||
func RegisterInt64AsyncGauge(descriptor MetricDescriptor) *Int64AsyncGaugeHandle {
|
||||
registerMetric(descriptor.Name, descriptor.Default)
|
||||
descriptor.Type = MetricTypeIntAsyncGauge
|
||||
descPtr := &descriptor
|
||||
metricsRegistry[descriptor.Name] = descPtr
|
||||
return (*Int64AsyncGaugeHandle)(descPtr)
|
||||
}
|
||||
|
||||
// snapshotMetricsRegistryForTesting snapshots the global data of the metrics
|
||||
// registry. Returns a cleanup function that sets the metrics registry to its
|
||||
// original state.
|
||||
func snapshotMetricsRegistryForTesting() func() {
|
||||
oldDefaultMetrics := DefaultMetrics
|
||||
oldRegisteredMetrics := registeredMetrics
|
||||
oldMetricsRegistry := metricsRegistry
|
||||
|
||||
registeredMetrics = make(map[string]bool)
|
||||
metricsRegistry = make(map[string]*MetricDescriptor)
|
||||
maps.Copy(registeredMetrics, registeredMetrics)
|
||||
maps.Copy(metricsRegistry, metricsRegistry)
|
||||
|
||||
return func() {
|
||||
DefaultMetrics = oldDefaultMetrics
|
||||
registeredMetrics = oldRegisteredMetrics
|
||||
metricsRegistry = oldMetricsRegistry
|
||||
}
|
||||
}
|
||||
318
experimental/stats/metricregistry_test.go
Normal file
318
experimental/stats/metricregistry_test.go
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 stats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
// TestPanic tests that registering two metrics with the same name across any
|
||||
// type of metric triggers a panic.
|
||||
func (s) TestPanic(t *testing.T) {
|
||||
cleanup := snapshotMetricsRegistryForTesting()
|
||||
defer cleanup()
|
||||
|
||||
want := "metric simple counter already registered"
|
||||
defer func() {
|
||||
if r := recover(); !strings.Contains(fmt.Sprint(r), want) {
|
||||
t.Errorf("expected panic contains %q, got %q", want, r)
|
||||
}
|
||||
}()
|
||||
desc := MetricDescriptor{
|
||||
// Type is not expected to be set from the registerer, but meant to be
|
||||
// set by the metric registry.
|
||||
Name: "simple counter",
|
||||
Description: "number of times recorded on tests",
|
||||
Unit: "{call}",
|
||||
}
|
||||
RegisterInt64Count(desc)
|
||||
RegisterInt64Gauge(desc)
|
||||
}
|
||||
|
||||
// TestInstrumentRegistry tests the metric registry. It registers testing only
|
||||
// metrics using the metric registry, and creates a fake metrics recorder which
|
||||
// uses these metrics. Using the handles returned from the metric registry, this
|
||||
// test records stats using the fake metrics recorder. Then, the test verifies
|
||||
// the persisted metrics data in the metrics recorder is what is expected. Thus,
|
||||
// this tests the interactions between the metrics recorder and the metrics
|
||||
// registry.
|
||||
func (s) TestMetricRegistry(t *testing.T) {
|
||||
cleanup := snapshotMetricsRegistryForTesting()
|
||||
defer cleanup()
|
||||
|
||||
intCountHandle1 := RegisterInt64Count(MetricDescriptor{
|
||||
Name: "simple counter",
|
||||
Description: "sum of all emissions from tests",
|
||||
Unit: "int",
|
||||
Labels: []string{"int counter label"},
|
||||
OptionalLabels: []string{"int counter optional label"},
|
||||
Default: false,
|
||||
})
|
||||
floatCountHandle1 := RegisterFloat64Count(MetricDescriptor{
|
||||
Name: "float counter",
|
||||
Description: "sum of all emissions from tests",
|
||||
Unit: "float",
|
||||
Labels: []string{"float counter label"},
|
||||
OptionalLabels: []string{"float counter optional label"},
|
||||
Default: false,
|
||||
})
|
||||
intHistoHandle1 := RegisterInt64Histo(MetricDescriptor{
|
||||
Name: "int histo",
|
||||
Description: "sum of all emissions from tests",
|
||||
Unit: "int",
|
||||
Labels: []string{"int histo label"},
|
||||
OptionalLabels: []string{"int histo optional label"},
|
||||
Default: false,
|
||||
})
|
||||
floatHistoHandle1 := RegisterFloat64Histo(MetricDescriptor{
|
||||
Name: "float histo",
|
||||
Description: "sum of all emissions from tests",
|
||||
Unit: "float",
|
||||
Labels: []string{"float histo label"},
|
||||
OptionalLabels: []string{"float histo optional label"},
|
||||
Default: false,
|
||||
})
|
||||
intGaugeHandle1 := RegisterInt64Gauge(MetricDescriptor{
|
||||
Name: "simple gauge",
|
||||
Description: "the most recent int emitted by test",
|
||||
Unit: "int",
|
||||
Labels: []string{"int gauge label"},
|
||||
OptionalLabels: []string{"int gauge optional label"},
|
||||
Default: false,
|
||||
})
|
||||
intUpDownCountHandle1 := RegisterInt64UpDownCount(MetricDescriptor{
|
||||
Name: "simple up down counter",
|
||||
Description: "current number of emissions from tests",
|
||||
Unit: "int",
|
||||
Labels: []string{"int up down counter label"},
|
||||
OptionalLabels: []string{"int up down counter optional label"},
|
||||
Default: false,
|
||||
})
|
||||
intAsyncGaugeHandle1 := RegisterInt64AsyncGauge(MetricDescriptor{
|
||||
Name: "simple async gauge",
|
||||
Description: "the most recent int emitted by test",
|
||||
Unit: "int",
|
||||
Labels: []string{"int async gauge label"},
|
||||
OptionalLabels: []string{"int async gauge optional label"},
|
||||
Default: false,
|
||||
})
|
||||
|
||||
fmr := newFakeMetricsRecorder(t)
|
||||
|
||||
intCountHandle1.Record(fmr, 1, []string{"some label value", "some optional label value"}...)
|
||||
// The Metric Descriptor in the handle should be able to identify the metric
|
||||
// information. This is the key passed to metrics recorder to identify
|
||||
// metric.
|
||||
if got := fmr.intValues[intCountHandle1.Descriptor()]; got != 1 {
|
||||
t.Fatalf("fmr.intValues[intCountHandle1.MetricDescriptor] got %v, want: %v", got, 1)
|
||||
}
|
||||
|
||||
intUpDownCountHandle1.Record(fmr, 2, []string{"some label value", "some optional label value"}...)
|
||||
// The Metric Descriptor in the handle should be able to identify the metric
|
||||
// information. This is the key passed to metrics recorder to identify
|
||||
// metric.
|
||||
if got := fmr.intValues[intUpDownCountHandle1.Descriptor()]; got != 2 {
|
||||
t.Fatalf("fmr.intValues[intUpDownCountHandle1.MetricDescriptor] got %v, want: %v", got, 2)
|
||||
}
|
||||
|
||||
floatCountHandle1.Record(fmr, 1.2, []string{"some label value", "some optional label value"}...)
|
||||
if got := fmr.floatValues[floatCountHandle1.Descriptor()]; got != 1.2 {
|
||||
t.Fatalf("fmr.floatValues[floatCountHandle1.MetricDescriptor] got %v, want: %v", got, 1.2)
|
||||
}
|
||||
|
||||
intHistoHandle1.Record(fmr, 3, []string{"some label value", "some optional label value"}...)
|
||||
if got := fmr.intValues[intHistoHandle1.Descriptor()]; got != 3 {
|
||||
t.Fatalf("fmr.intValues[intHistoHandle1.MetricDescriptor] got %v, want: %v", got, 3)
|
||||
}
|
||||
|
||||
floatHistoHandle1.Record(fmr, 4.3, []string{"some label value", "some optional label value"}...)
|
||||
if got := fmr.floatValues[floatHistoHandle1.Descriptor()]; got != 4.3 {
|
||||
t.Fatalf("fmr.floatValues[floatHistoHandle1.MetricDescriptor] got %v, want: %v", got, 4.3)
|
||||
}
|
||||
|
||||
intGaugeHandle1.Record(fmr, 7, []string{"some label value", "some optional label value"}...)
|
||||
if got := fmr.intValues[intGaugeHandle1.Descriptor()]; got != 7 {
|
||||
t.Fatalf("fmr.intValues[intGaugeHandle1.MetricDescriptor] got %v, want: %v", got, 7)
|
||||
}
|
||||
intAsyncGaugeHandle1.Record(fmr, 9, []string{"some label value", "some optional label value"}...)
|
||||
if got := fmr.intValues[intAsyncGaugeHandle1.Descriptor()]; got != 9 {
|
||||
t.Fatalf("fmr.intValues[intAsyncGaugeHandle1.MetricDescriptor] got %v, want: %v", got, 9)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestUpDownCounts(t *testing.T) {
|
||||
cleanup := snapshotMetricsRegistryForTesting()
|
||||
defer cleanup()
|
||||
|
||||
intUpDownCountHandle1 := RegisterInt64UpDownCount(MetricDescriptor{
|
||||
Name: "simple up down counter",
|
||||
Description: "current number of emissions from tests",
|
||||
Unit: "int",
|
||||
Labels: []string{"int up down counter label"},
|
||||
OptionalLabels: []string{"int up down counter optional label"},
|
||||
Default: false,
|
||||
})
|
||||
|
||||
fmr := newFakeMetricsRecorder(t)
|
||||
intUpDownCountHandle1.Record(fmr, 2, []string{"up down value", "some optional label value"}...)
|
||||
intUpDownCountHandle1.Record(fmr, -1, []string{"up down value", "some optional label value"}...)
|
||||
|
||||
if got := fmr.intValues[intUpDownCountHandle1.Descriptor()]; got != 1 {
|
||||
t.Fatalf("fmr.intValues[intUpDownCountHandle1.MetricDescriptor] got %v, want: %v", got, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNumerousIntCounts tests numerous int count metrics registered onto the
|
||||
// metric registry. A component (simulated by test) should be able to record on
|
||||
// the different registered int count metrics.
|
||||
func (s) TestNumerousIntCounts(t *testing.T) {
|
||||
cleanup := snapshotMetricsRegistryForTesting()
|
||||
defer cleanup()
|
||||
|
||||
intCountHandle1 := RegisterInt64Count(MetricDescriptor{
|
||||
Name: "int counter",
|
||||
Description: "sum of all emissions from tests",
|
||||
Unit: "int",
|
||||
Labels: []string{"int counter label"},
|
||||
OptionalLabels: []string{"int counter optional label"},
|
||||
Default: false,
|
||||
})
|
||||
intCountHandle2 := RegisterInt64Count(MetricDescriptor{
|
||||
Name: "int counter 2",
|
||||
Description: "sum of all emissions from tests",
|
||||
Unit: "int",
|
||||
Labels: []string{"int counter label"},
|
||||
OptionalLabels: []string{"int counter optional label"},
|
||||
Default: false,
|
||||
})
|
||||
intCountHandle3 := RegisterInt64Count(MetricDescriptor{
|
||||
Name: "int counter 3",
|
||||
Description: "sum of all emissions from tests",
|
||||
Unit: "int",
|
||||
Labels: []string{"int counter label"},
|
||||
OptionalLabels: []string{"int counter optional label"},
|
||||
Default: false,
|
||||
})
|
||||
|
||||
fmr := newFakeMetricsRecorder(t)
|
||||
|
||||
intCountHandle1.Record(fmr, 1, []string{"some label value", "some optional label value"}...)
|
||||
got := []int64{fmr.intValues[intCountHandle1.Descriptor()], fmr.intValues[intCountHandle2.Descriptor()], fmr.intValues[intCountHandle3.Descriptor()]}
|
||||
want := []int64{1, 0, 0}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("fmr.intValues (-got, +want): %v", diff)
|
||||
}
|
||||
|
||||
intCountHandle2.Record(fmr, 1, []string{"some label value", "some optional label value"}...)
|
||||
got = []int64{fmr.intValues[intCountHandle1.Descriptor()], fmr.intValues[intCountHandle2.Descriptor()], fmr.intValues[intCountHandle3.Descriptor()]}
|
||||
want = []int64{1, 1, 0}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("fmr.intValues (-got, +want): %v", diff)
|
||||
}
|
||||
|
||||
intCountHandle3.Record(fmr, 1, []string{"some label value", "some optional label value"}...)
|
||||
got = []int64{fmr.intValues[intCountHandle1.Descriptor()], fmr.intValues[intCountHandle2.Descriptor()], fmr.intValues[intCountHandle3.Descriptor()]}
|
||||
want = []int64{1, 1, 1}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("fmr.intValues (-got, +want): %v", diff)
|
||||
}
|
||||
|
||||
intCountHandle3.Record(fmr, 1, []string{"some label value", "some optional label value"}...)
|
||||
got = []int64{fmr.intValues[intCountHandle1.Descriptor()], fmr.intValues[intCountHandle2.Descriptor()], fmr.intValues[intCountHandle3.Descriptor()]}
|
||||
want = []int64{1, 1, 2}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("fmr.intValues (-got, +want): %v", diff)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeMetricsRecorder struct {
|
||||
UnimplementedMetricsRecorder
|
||||
t *testing.T
|
||||
|
||||
intValues map[*MetricDescriptor]int64
|
||||
floatValues map[*MetricDescriptor]float64
|
||||
}
|
||||
|
||||
// newFakeMetricsRecorder returns a fake metrics recorder based off the current
|
||||
// state of global metric registry.
|
||||
func newFakeMetricsRecorder(t *testing.T) *fakeMetricsRecorder {
|
||||
fmr := &fakeMetricsRecorder{
|
||||
t: t,
|
||||
intValues: make(map[*MetricDescriptor]int64),
|
||||
floatValues: make(map[*MetricDescriptor]float64),
|
||||
}
|
||||
return fmr
|
||||
}
|
||||
|
||||
// verifyLabels verifies that the labels received are of the expected length.
|
||||
func verifyLabels(t *testing.T, labelsWant []string, optionalLabelsWant []string, labelsGot []string) {
|
||||
if len(labelsWant)+len(optionalLabelsWant) != len(labelsGot) {
|
||||
t.Fatalf("length of optional labels expected did not match got %v, want %v", len(labelsGot), len(labelsWant)+len(optionalLabelsWant))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *fakeMetricsRecorder) RecordInt64Count(handle *Int64CountHandle, incr int64, labels ...string) {
|
||||
verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels)
|
||||
r.intValues[handle.Descriptor()] += incr
|
||||
}
|
||||
|
||||
func (r *fakeMetricsRecorder) RecordFloat64Count(handle *Float64CountHandle, incr float64, labels ...string) {
|
||||
verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels)
|
||||
r.floatValues[handle.Descriptor()] += incr
|
||||
}
|
||||
|
||||
func (r *fakeMetricsRecorder) RecordInt64Histo(handle *Int64HistoHandle, incr int64, labels ...string) {
|
||||
verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels)
|
||||
r.intValues[handle.Descriptor()] += incr
|
||||
}
|
||||
|
||||
func (r *fakeMetricsRecorder) RecordFloat64Histo(handle *Float64HistoHandle, incr float64, labels ...string) {
|
||||
verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels)
|
||||
r.floatValues[handle.Descriptor()] += incr
|
||||
}
|
||||
|
||||
func (r *fakeMetricsRecorder) RecordInt64Gauge(handle *Int64GaugeHandle, incr int64, labels ...string) {
|
||||
verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels)
|
||||
r.intValues[handle.Descriptor()] += incr
|
||||
}
|
||||
|
||||
func (r *fakeMetricsRecorder) RecordInt64UpDownCount(handle *Int64UpDownCountHandle, incr int64, labels ...string) {
|
||||
verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels)
|
||||
r.intValues[handle.Descriptor()] += incr
|
||||
}
|
||||
|
||||
func (r *fakeMetricsRecorder) RecordInt64AsyncGauge(handle *Int64AsyncGaugeHandle, val int64, labels ...string) {
|
||||
verifyLabels(r.t, handle.Descriptor().Labels, handle.Descriptor().OptionalLabels, labels)
|
||||
// Async gauges in OTel are "Observer" instruments; they report
|
||||
// the current state of the world every cycle, they do not accumulate deltas.
|
||||
r.intValues[handle.Descriptor()] = val
|
||||
}
|
||||
148
experimental/stats/metrics.go
Normal file
148
experimental/stats/metrics.go
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 stats contains experimental metrics/stats API's.
|
||||
package stats
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/stats"
|
||||
)
|
||||
|
||||
type customLabelKey struct{}
|
||||
|
||||
// NewContextWithCustomLabel returns a new context with the provided custom label
|
||||
// attached. The label will be propagated to all metric instruments specified in gRFC A108.
|
||||
func NewContextWithCustomLabel(ctx context.Context, label string) context.Context {
|
||||
return context.WithValue(ctx, customLabelKey{}, label)
|
||||
}
|
||||
|
||||
// CustomLabelFromContext returns the custom label from the context if it exists.
|
||||
// If the custom label is not present, it returns an empty string.
|
||||
func CustomLabelFromContext(ctx context.Context) string {
|
||||
label, _ := ctx.Value(customLabelKey{}).(string)
|
||||
return label
|
||||
}
|
||||
|
||||
// MetricsRecorder records on metrics derived from metric registry.
|
||||
// Implementors must embed UnimplementedMetricsRecorder.
|
||||
type MetricsRecorder interface {
|
||||
// RecordInt64Count records the measurement alongside labels on the int
|
||||
// count associated with the provided handle.
|
||||
RecordInt64Count(handle *Int64CountHandle, incr int64, labels ...string)
|
||||
// RecordFloat64Count records the measurement alongside labels on the float
|
||||
// count associated with the provided handle.
|
||||
RecordFloat64Count(handle *Float64CountHandle, incr float64, labels ...string)
|
||||
// RecordInt64Histo records the measurement alongside labels on the int
|
||||
// histo associated with the provided handle.
|
||||
RecordInt64Histo(handle *Int64HistoHandle, incr int64, labels ...string)
|
||||
// RecordFloat64Histo records the measurement alongside labels on the float
|
||||
// histo associated with the provided handle.
|
||||
RecordFloat64Histo(handle *Float64HistoHandle, incr float64, labels ...string)
|
||||
// RecordInt64Gauge records the measurement alongside labels on the int
|
||||
// gauge associated with the provided handle.
|
||||
RecordInt64Gauge(handle *Int64GaugeHandle, incr int64, labels ...string)
|
||||
// RecordInt64UpDownCounter records the measurement alongside labels on the int
|
||||
// count associated with the provided handle.
|
||||
RecordInt64UpDownCount(handle *Int64UpDownCountHandle, incr int64, labels ...string)
|
||||
// RegisterAsyncReporter registers a reporter to produce metric values for
|
||||
// only the listed descriptors. The returned function must be called when
|
||||
// the metrics are no longer needed, which will remove the reporter. The
|
||||
// returned method needs to be idempotent and concurrent safe.
|
||||
RegisterAsyncReporter(reporter AsyncMetricReporter, descriptors ...AsyncMetric) func()
|
||||
|
||||
// EnforceMetricsRecorderEmbedding is included to force implementers to embed
|
||||
// another implementation of this interface, allowing gRPC to add methods
|
||||
// without breaking users.
|
||||
internal.EnforceMetricsRecorderEmbedding
|
||||
}
|
||||
|
||||
// AsyncMetricReporter is an interface for types that record metrics asynchronously
|
||||
// for the set of descriptors they are registered with. The AsyncMetricsRecorder
|
||||
// parameter is used to record values for these metrics.
|
||||
//
|
||||
// Implementations must make unique recordings across all registered
|
||||
// AsyncMetricReporters. Meaning, they should not report values for a metric with
|
||||
// the same attributes as another AsyncMetricReporter will report.
|
||||
//
|
||||
// Implementations must be concurrent-safe.
|
||||
type AsyncMetricReporter interface {
|
||||
// Report records metric values using the provided recorder.
|
||||
Report(AsyncMetricsRecorder) error
|
||||
}
|
||||
|
||||
// AsyncMetricReporterFunc is an adapter to allow the use of ordinary functions as
|
||||
// AsyncMetricReporters.
|
||||
type AsyncMetricReporterFunc func(AsyncMetricsRecorder) error
|
||||
|
||||
// Report calls f(r).
|
||||
func (f AsyncMetricReporterFunc) Report(r AsyncMetricsRecorder) error {
|
||||
return f(r)
|
||||
}
|
||||
|
||||
// AsyncMetricsRecorder records on asynchronous metrics derived from metric registry.
|
||||
type AsyncMetricsRecorder interface {
|
||||
// RecordInt64AsyncGauge records the measurement alongside labels on the int
|
||||
// count associated with the provided handle asynchronously
|
||||
RecordInt64AsyncGauge(handle *Int64AsyncGaugeHandle, incr int64, labels ...string)
|
||||
}
|
||||
|
||||
// Metrics is an experimental legacy alias of the now-stable stats.MetricSet.
|
||||
// Metrics will be deleted in a future release.
|
||||
type Metrics = stats.MetricSet
|
||||
|
||||
// Metric was replaced by direct usage of strings.
|
||||
type Metric = string
|
||||
|
||||
// NewMetrics is an experimental legacy alias of the now-stable
|
||||
// stats.NewMetricSet. NewMetrics will be deleted in a future release.
|
||||
func NewMetrics(metrics ...Metric) *Metrics {
|
||||
return stats.NewMetricSet(metrics...)
|
||||
}
|
||||
|
||||
// UnimplementedMetricsRecorder must be embedded to have forward compatible implementations.
|
||||
type UnimplementedMetricsRecorder struct {
|
||||
internal.EnforceMetricsRecorderEmbedding
|
||||
}
|
||||
|
||||
// RecordInt64Count provides a no-op implementation.
|
||||
func (UnimplementedMetricsRecorder) RecordInt64Count(*Int64CountHandle, int64, ...string) {}
|
||||
|
||||
// RecordFloat64Count provides a no-op implementation.
|
||||
func (UnimplementedMetricsRecorder) RecordFloat64Count(*Float64CountHandle, float64, ...string) {}
|
||||
|
||||
// RecordInt64Histo provides a no-op implementation.
|
||||
func (UnimplementedMetricsRecorder) RecordInt64Histo(*Int64HistoHandle, int64, ...string) {}
|
||||
|
||||
// RecordFloat64Histo provides a no-op implementation.
|
||||
func (UnimplementedMetricsRecorder) RecordFloat64Histo(*Float64HistoHandle, float64, ...string) {}
|
||||
|
||||
// RecordInt64Gauge provides a no-op implementation.
|
||||
func (UnimplementedMetricsRecorder) RecordInt64Gauge(*Int64GaugeHandle, int64, ...string) {}
|
||||
|
||||
// RecordInt64UpDownCount provides a no-op implementation.
|
||||
func (UnimplementedMetricsRecorder) RecordInt64UpDownCount(*Int64UpDownCountHandle, int64, ...string) {
|
||||
}
|
||||
|
||||
// RegisterAsyncReporter provides a no-op implementation.
|
||||
func (UnimplementedMetricsRecorder) RegisterAsyncReporter(AsyncMetricReporter, ...AsyncMetric) func() {
|
||||
// No-op: Return an empty function to ensure caller doesn't panic on nil function call
|
||||
return func() {}
|
||||
}
|
||||
42
experimental/stats/telemetry/labels.go
Normal file
42
experimental/stats/telemetry/labels.go
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2026 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 telemetry defines stats APIs for interacting with
|
||||
// telemetry labels.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: All APIs in this package are experimental and may be changed or
|
||||
// removed in a later release.
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
stats "google.golang.org/grpc/internal/stats"
|
||||
)
|
||||
|
||||
// NewContextWithLabelCallback registers a callback function that is executed
|
||||
// whenever telemetry labels are updated. This does _not_ require opentelemetry
|
||||
// instrumentation to be configured on the client or server.
|
||||
//
|
||||
// WARNING: Callbacks are executed on the RPC path so users should be mindful
|
||||
// of the potential performance impact when this is eventually executed.
|
||||
func NewContextWithLabelCallback(ctx context.Context, callback stats.LabelCallback) context.Context {
|
||||
return stats.RegisterTelemetryLabelCallback(ctx, callback)
|
||||
}
|
||||
Reference in New Issue
Block a user