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

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

View File

@@ -0,0 +1,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
}

View 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)
}
})
}
}

View 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
}

View 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)
}
})
}
}

View 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
}

View 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
}

View 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)
}
})
}
}

View 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,
}
}

View 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)
}
}

View 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)
}

View 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)
}
}
})
}
}

View 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...)
}

View 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
}

View 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)
}

View 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
}
}

View 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
}

View 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() {}
}

View 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)
}