Skip to content
5 changes: 4 additions & 1 deletion internal/gengapic/client_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ func TestClientOpt(t *testing.T) {
gRPCServiceConfig: grpcConf,
// Showcase would enable MTLS if we went through legacy enablements, so add it explicitly here.
featureEnablement: map[featureID]struct{}{
MTLSHardBoundTokensFeature: struct{}{},
MTLSHardBoundTokensFeature: struct{}{},
OpenTelemetryTracingFeature: struct{}{},
},
},
}
Expand Down Expand Up @@ -582,6 +583,7 @@ func TestClientInit(t *testing.T) {
}
g.mixins = tst.mixins
g.cfg.APIServiceConfig = &serviceconfig.Service{
Name: "foo.googleapis.com",
Apis: []*apipb.Api{
{Name: "foo.bar.Baz"},
{Name: "google.iam.v1.IAMPolicy"},
Expand All @@ -600,6 +602,7 @@ func TestClientInit(t *testing.T) {
}

g.reset()
g.cfg.featureEnablement = map[featureID]struct{}{OpenTelemetryTracingFeature: {}}
sm := snippets.NewMetadata("mypackage", "github.com/googleapis/mypackage", "mypackagego")
sm.AddService(tst.serv.GetName(), "mypackage.googleapis.com")
for _, m := range tst.serv.GetMethod() {
Expand Down
6 changes: 6 additions & 0 deletions internal/gengapic/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@ const (
WrapperTypesForPageSizeFeature featureID = "wrapper_types_for_page_size"
OrderedRoutingHeadersFeature featureID = "ordered_routing_headers"
MTLSHardBoundTokensFeature featureID = "mtls_hard_bound_tokens"
OpenTelemetryTracingFeature featureID = "open_telemetry_tracing"
)

// featureRegistry contains the registry of defined features.
// Introducing a new capability to the generator generally starts here, as features
// must be registered to be enabled. This should not be modified at runtime. Those
// who attempt to do so will be given a stern talking to.
var featureRegistry = map[featureID]*featureInfo{

OpenTelemetryTracingFeature: {
Description: "Enable OpenTelemetry tracing support (Service Identity, Resource Names, URL Templates).",
TrackingID: "b/467342602,b/467403185",
},
MTLSHardBoundTokensFeature: {
Description: "support MTLS hard bound tokens",
TrackingID: "b/327916505",
Expand Down
1 change: 1 addition & 0 deletions internal/gengapic/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func TestAutoPopulatedFields(t *testing.T) {
g.imports = map[pbinfo.ImportSpec]bool{}

g.cfg.APIServiceConfig = &serviceconfig.Service{
Name: "foo.googleapis.com",
Publishing: &annotations.Publishing{
MethodSettings: []*annotations.MethodSettings{
{
Expand Down
104 changes: 104 additions & 0 deletions internal/gengapic/gengapic.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,10 +498,62 @@ func (g *generator) insertRequestHeaders(m *descriptorpb.MethodDescriptorProto,
case grpc:
p("hds = append(c.xGoogHeaders, hds...)")
p("ctx = gax.InsertMetadataIntoOutgoingContext(ctx, hds...)")
if g.featureEnabled(OpenTelemetryTracingFeature) {
resField := g.resourceNameField(m)
if resField != "" {
p("if gax.IsFeatureEnabled(\"TRACING\") {")
// For Standard APIs (AIP-122 compliant), for both gRPC and HTTP transports,
// the expression fieldGetter(resField) returns an accessor for the full
// canonical resource name (e.g., "projects/p/secrets/s"). For non-compliant
// APIs (missing the resource_reference annotation), an empty string is returned.

// Prepend the service host if available
serv := g.descInfo.ParentElement[m].(*descriptorpb.ServiceDescriptorProto)
host := ""
if proto.HasExtension(serv.Options, annotations.E_DefaultHost) {
host = proto.GetExtension(serv.Options, annotations.E_DefaultHost).(string)
}

if host != "" {
p(` ctx = metadata.AppendToOutgoingContext(ctx, "gcp.resource.name", fmt.Sprintf("//%s/%%v", req%s))`, host, fieldGetter(resField))
} else {
p(` ctx = metadata.AppendToOutgoingContext(ctx, "gcp.resource.name", fmt.Sprintf("%%v", req%s))`, fieldGetter(resField))
}
p("}")
g.imports[pbinfo.ImportSpec{Path: "google.golang.org/grpc/metadata"}] = true
g.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
}
}
case rest:
p(`hds = append(c.xGoogHeaders, hds...)`)
p(`hds = append(hds, "Content-Type", "application/json")`)
p(`headers := gax.BuildHeaders(ctx, hds...)`)
if g.featureEnabled(OpenTelemetryTracingFeature) {
resField := g.resourceNameField(m)
if resField != "" {
p("if gax.IsFeatureEnabled(\"TRACING\") {")
// For Standard APIs (AIP-122 compliant), for both gRPC and HTTP transports,
// the expression fieldGetter(resField) returns an accessor for the full
// canonical resource name (e.g., "projects/p/secrets/s"). For non-compliant
// APIs (missing the resource_reference annotation), an empty string is returned.

// Prepend the service host if available
serv := g.descInfo.ParentElement[m].(*descriptorpb.ServiceDescriptorProto)
host := ""
if proto.HasExtension(serv.Options, annotations.E_DefaultHost) {
host = proto.GetExtension(serv.Options, annotations.E_DefaultHost).(string)
}

if host != "" {
p(` ctx = metadata.AppendToOutgoingContext(ctx, "gcp.resource.name", fmt.Sprintf("//%s/%%v", req%s))`, host, fieldGetter(resField))
} else {
p(` ctx = metadata.AppendToOutgoingContext(ctx, "gcp.resource.name", fmt.Sprintf("%%v", req%s))`, fieldGetter(resField))
}
p("}")
g.imports[pbinfo.ImportSpec{Path: "google.golang.org/grpc/metadata"}] = true
g.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
}
}
}
g.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
g.imports[pbinfo.ImportSpec{Path: "net/url"}] = true
Expand Down Expand Up @@ -816,3 +868,55 @@ func parseDynamicRequestHeaders(m *descriptorpb.MethodDescriptorProto) [][]strin

return matches
}

// resourceNameField returns the name of the field in the input message
// that carries a google.api.resource_reference annotation.
// If multiple fields match, it prioritizes the one that also appears in the HTTP path.
// If no input type or associated attributes are found, it returns an empty string.
func (g *generator) resourceNameField(m *descriptorpb.MethodDescriptorProto) string {
if m.GetInputType() == "" {
return ""
}
inType := g.descInfo.Type[m.GetInputType()]
if inType == nil {
return ""
}
msg, ok := inType.(*descriptorpb.DescriptorProto)
if !ok {
return ""
}

var candidates []string
for _, f := range msg.GetField() {
if proto.HasExtension(f.GetOptions(), annotations.E_ResourceReference) {
candidates = append(candidates, f.GetName())
}
}

if len(candidates) == 0 {
return ""
}
if len(candidates) == 1 {
return candidates[0]
}

// Tie-breaking: check against HTTP path variables.
// parseImplicitRequestHeaders uses headerParamRegexp (which has one capturing
// group) to find variables in the path. h[1] corresponds to that capturing group
// and contains the variable's string name (e.g., "name", "parent" or "project").
pathParams := make(map[string]bool)
headers := parseImplicitRequestHeaders(m)
for _, h := range headers {
if len(h) > 1 {
pathParams[h[1]] = true
}
}

for _, c := range candidates {
if pathParams[c] {
return c
}
}

return candidates[0]
}
68 changes: 43 additions & 25 deletions internal/gengapic/gengapic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,20 @@ func TestGenGRPCMethods(t *testing.T) {
optsUUID4 := &descriptorpb.FieldOptions{}
proto.SetExtension(optsUUID4, annotations.E_FieldInfo, &annotations.FieldInfo{Format: annotations.FieldInfo_UUID4})

optsResRef := &descriptorpb.FieldOptions{}
proto.SetExtension(optsResRef, annotations.E_ResourceReference, &annotations.ResourceReference{
// "{service}.googleapis.com/{Resource}" is the AIP-122 format for the google.api.resource_reference annotation.
Type: "foo.googleapis.com/Bar",
})

inputType := &descriptorpb.DescriptorProto{
Name: proto.String("InputType"),
Field: []*descriptorpb.FieldDescriptorProto{
{
Name: proto.String("other"),
Type: typep(descriptorpb.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL),
Name: proto.String("other"),
Type: typep(descriptorpb.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL),
Options: optsResRef,
},
{
Name: proto.String("another"),
Expand Down Expand Up @@ -322,12 +329,17 @@ func TestGenGRPCMethods(t *testing.T) {
},
}
serv := &descriptorpb.ServiceDescriptorProto{
Name: proto.String("Foo"),
Name: proto.String("Foo"),
Options: &descriptorpb.ServiceOptions{},
}
proto.SetExtension(serv.Options, annotations.E_DefaultHost, "foo.googleapis.com")

var g generator
g.cfg = &generatorConfig{
pkgName: "pkg",
featureEnablement: map[featureID]struct{}{
OpenTelemetryTracingFeature: {},
},
APIServiceConfig: &serviceconfig.Service{
Publishing: &annotations.Publishing{
MethodSettings: []*annotations.MethodSettings{
Expand Down Expand Up @@ -407,24 +419,25 @@ func TestGenGRPCMethods(t *testing.T) {
Options: opts,
},
imports: map[pbinfo.ImportSpec]bool{
{Path: "fmt"}: true,
{Path: "github.com/google/uuid"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
{Path: "fmt"}: true,
{Path: "github.com/google/uuid"}: true,
{Path: "google.golang.org/grpc/metadata"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
},
},
{
}, {
m: &descriptorpb.MethodDescriptorProto{
Name: proto.String("GetOneThing"),
InputType: proto.String(".my.pkg.InputType"),
OutputType: proto.String(".my.pkg.OutputType"),
Options: opts,
},
imports: map[pbinfo.ImportSpec]bool{
{Path: "fmt"}: true,
{Path: "github.com/google/uuid"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
{Path: "fmt"}: true,
{Path: "github.com/google/uuid"}: true,
{Path: "google.golang.org/grpc/metadata"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
},
},
{
Expand All @@ -437,12 +450,12 @@ func TestGenGRPCMethods(t *testing.T) {
imports: map[pbinfo.ImportSpec]bool{
{Path: "fmt"}: true,
{Path: "google.golang.org/api/iterator"}: true,
{Path: "google.golang.org/grpc/metadata"}: true,
{Path: "google.golang.org/protobuf/proto"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
},
},
{
}, {
m: &descriptorpb.MethodDescriptorProto{
Name: proto.String("GetManyThingsOptional"),
InputType: proto.String(".my.pkg.PageInputTypeOptional"),
Expand All @@ -452,6 +465,7 @@ func TestGenGRPCMethods(t *testing.T) {
imports: map[pbinfo.ImportSpec]bool{
{Path: "fmt"}: true,
{Path: "google.golang.org/api/iterator"}: true,
{Path: "google.golang.org/grpc/metadata"}: true,
{Path: "google.golang.org/protobuf/proto"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
Expand All @@ -466,9 +480,10 @@ func TestGenGRPCMethods(t *testing.T) {
Options: opts,
},
imports: map[pbinfo.ImportSpec]bool{
{Path: "fmt"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
{Path: "fmt"}: true,
{Path: "google.golang.org/grpc/metadata"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
},
},
{
Expand Down Expand Up @@ -505,11 +520,12 @@ func TestGenGRPCMethods(t *testing.T) {
Options: optsGetAnotherThing,
},
imports: map[pbinfo.ImportSpec]bool{
{Path: "fmt"}: true,
{Path: "net/url"}: true,
{Path: "regexp"}: true,
{Path: "strings"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
{Path: "fmt"}: true,
{Path: "google.golang.org/grpc/metadata"}: true,
{Path: "net/url"}: true,
{Path: "regexp"}: true,
{Path: "strings"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
},
},
// Test for empty dynamic routing annotation, so no headers should be sent.
Expand Down Expand Up @@ -712,8 +728,10 @@ func TestGenOperationBuilders(t *testing.T) {
},
}
serv := &descriptorpb.ServiceDescriptorProto{
Name: proto.String("Foo"),
Name: proto.String("Foo"),
Options: &descriptorpb.ServiceOptions{},
}
proto.SetExtension(serv.Options, annotations.E_DefaultHost, "foo.googleapis.com")

var g generator
g.imports = map[pbinfo.ImportSpec]bool{}
Expand Down
9 changes: 9 additions & 0 deletions internal/gengapic/gengrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,15 @@ func (g *generator) grpcClientUtilities(serv *descriptorpb.ServiceDescriptorProt
g.serviceDoc(serv, false) // exclude API version docs
p("func New%[1]sClient(ctx context.Context, opts ...option.ClientOption) (*%[1]sClient, error) {", servName)
p(" clientOpts := default%[1]sGRPCClientOptions()", servName)
if g.featureEnabled(OpenTelemetryTracingFeature) {
p(" if gax.IsFeatureEnabled(\"TRACING\") {")
p(" clientOpts = append(clientOpts, internaloption.WithTelemetryAttributes(map[string]string{")
p(" \"gcp.client.service\": %q,", strings.Split(g.cfg.APIServiceConfig.GetName(), ".")[0])
p(" \"gcp.client.version\": getVersionClient(),")
p(" \"gcp.client.repo\": \"googleapis/google-cloud-go\",")
p(" }))")
p(" }")
}

p(" if new%sClientHook != nil {", servName)
p(" hookOpts, err := new%sClientHook(ctx, clientHookParams{})", servName)
Expand Down
6 changes: 4 additions & 2 deletions internal/gengapic/gengrpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ func TestServiceRenaming(t *testing.T) {
var g generator
g.imports = map[pbinfo.ImportSpec]bool{}
g.cfg = &generatorConfig{
pkgName: "pkg",
transports: []transport{grpc},
pkgName: "pkg",
transports: []transport{grpc},
featureEnablement: map[featureID]struct{}{OpenTelemetryTracingFeature: {}},
APIServiceConfig: &serviceconfig.Service{
Name: "foo.googleapis.com",
Publishing: &annotations.Publishing{
LibrarySettings: []*annotations.ClientLibrarySettings{
{
Expand Down
Loading
Loading