Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e84910f
feat(gengapic): configure dynamic_resource_heuristics feature flag
quartzmo Mar 25, 2026
4635943
feat(gengapic): add BuildHeuristicVocabulary for resource name learning
quartzmo Mar 25, 2026
fdd8be1
feat(gengapic): add IdentifyHeuristicTarget for path pattern resolution
quartzmo Mar 25, 2026
2169aac
feat(gengapic): integrate heuristic fallback for unannotated resource…
quartzmo Mar 25, 2026
6e54d77
goimports -w .
quartzmo Mar 25, 2026
eb06b3c
feat(gengapic): add dynamic resource name heuristics
quartzmo Mar 26, 2026
d84f470
fix docs
quartzmo Mar 26, 2026
6b3f4e1
fix golint
quartzmo Mar 27, 2026
fd6fc25
gofmt -w .
quartzmo Mar 27, 2026
02e939e
add heuristics.go and heuristics_test.go to gengapic/BUILD.bazel
quartzmo Mar 27, 2026
46c8fa1
move BuildHeuristicVocabulary call to top of newGenerator and delete …
quartzmo Mar 27, 2026
437d632
update gcp.resource.name to gcp.destination.resource.id
quartzmo Mar 27, 2026
b8dea22
extract varNameRegex to package var
quartzmo Mar 27, 2026
afebda5
update logic for custom verb stripping on literals
quartzmo Mar 27, 2026
1aa4ee4
unexport declarations in heuristics.go
quartzmo Mar 27, 2026
abe5453
replace metadata.AppendToOutgoingContext with callctx.WithTelemetryCo…
quartzmo Mar 27, 2026
1c871f1
goimports -w .
quartzmo Mar 27, 2026
4d884be
update gcp.destination.resource.id to resource_name and url.template …
quartzmo Mar 30, 2026
ae1b469
refactor to injectTelemetryContext
quartzmo Mar 30, 2026
8bf0e4f
unify generation-time telemetry flags
quartzmo Mar 31, 2026
43545b9
add TestResourceNameField_FeatureFlag
quartzmo Mar 31, 2026
f5ade82
Merge branch 'main' into feature/heuristics-integration
quartzmo Mar 31, 2026
390f6db
use F_open_telemetry_attributes in showcase.bash
quartzmo Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/gengapic/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ go_library(
"gengrpc.go",
"genrest.go",
"helpers.go",
"heuristics.go",
"imports.go",
"lro.go",
"markdown.go",
Expand Down Expand Up @@ -74,6 +75,7 @@ go_test(
"gengapic_test.go",
"genrest_test.go",
"helpers_test.go",
"heuristics_test.go",
"imports_test.go",
"markdown_test.go",
"metadata_test.go",
Expand Down
18 changes: 9 additions & 9 deletions internal/gengapic/client_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +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{}{},
OpenTelemetryTracingFeature: struct{}{},
MTLSHardBoundTokensFeature: struct{}{},
OpenTelemetryAttributesFeature: struct{}{},
},
},
}
Expand Down Expand Up @@ -458,7 +458,7 @@ func TestClientInit(t *testing.T) {
{Path: "log/slog"}: true,
},
wantNumSnps: 6,
features: []featureID{OpenTelemetryTracingFeature},
features: []featureID{OpenTelemetryAttributesFeature},
},
{
tstName: "foo_client_init_logging",
Expand All @@ -480,7 +480,7 @@ func TestClientInit(t *testing.T) {
{Path: "log/slog"}: true,
},
wantNumSnps: 6,
features: []featureID{OpenTelemetryLoggingFeature},
features: []featureID{OpenTelemetryAttributesFeature},
},
{
tstName: "foo_client_init_metrics",
Expand All @@ -502,7 +502,7 @@ func TestClientInit(t *testing.T) {
{Path: "log/slog"}: true,
},
wantNumSnps: 6,
features: []featureID{OpenTelemetryMetricsFeature},
features: []featureID{OpenTelemetryAttributesFeature},
},
{
tstName: "foo_client_init_all_telemetry",
Expand All @@ -524,7 +524,7 @@ func TestClientInit(t *testing.T) {
{Path: "log/slog"}: true,
},
wantNumSnps: 6,
features: []featureID{OpenTelemetryTracingFeature, OpenTelemetryLoggingFeature, OpenTelemetryMetricsFeature},
features: []featureID{OpenTelemetryAttributesFeature},
},
{
tstName: "foo_rest_client_init",
Expand Down Expand Up @@ -595,7 +595,7 @@ func TestClientInit(t *testing.T) {
{Path: "log/slog"}: true,
},
wantNumSnps: 6,
features: []featureID{OpenTelemetryTracingFeature, OpenTelemetryMetricsFeature},
features: []featureID{OpenTelemetryAttributesFeature},
},
{
tstName: "empty_client_init",
Expand Down Expand Up @@ -694,7 +694,7 @@ func TestClientInit(t *testing.T) {
setExt: func() (protoreflect.ExtensionType, interface{}) {
return extendedops.E_OperationService, opS.GetName()
},
features: []featureID{OpenTelemetryTracingFeature, OpenTelemetryMetricsFeature},
features: []featureID{OpenTelemetryAttributesFeature},
},
} {
t.Run(tst.tstName, func(t *testing.T) {
Expand Down Expand Up @@ -763,7 +763,7 @@ func TestClientInit(t *testing.T) {
g.reset()
g.cfg.featureEnablement = make(map[featureID]struct{})
if len(tst.features) == 0 {
g.cfg.featureEnablement[OpenTelemetryTracingFeature] = struct{}{}
g.cfg.featureEnablement[OpenTelemetryAttributesFeature] = struct{}{}
}
for _, f := range tst.features {
if f == "" {
Expand Down
24 changes: 10 additions & 14 deletions internal/gengapic/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ type featureInfo struct {

// Define feature ID strings here. More details about features are kept in the featureRegistry map.
const (
WrapperTypesForPageSizeFeature featureID = "wrapper_types_for_page_size"
OrderedRoutingHeadersFeature featureID = "ordered_routing_headers"
MTLSHardBoundTokensFeature featureID = "mtls_hard_bound_tokens"
OpenTelemetryTracingFeature featureID = "open_telemetry_tracing"
OpenTelemetryLoggingFeature featureID = "open_telemetry_logging"
OpenTelemetryMetricsFeature featureID = "open_telemetry_metrics"
WrapperTypesForPageSizeFeature featureID = "wrapper_types_for_page_size"
OrderedRoutingHeadersFeature featureID = "ordered_routing_headers"
MTLSHardBoundTokensFeature featureID = "mtls_hard_bound_tokens"
OpenTelemetryAttributesFeature featureID = "open_telemetry_attributes"
DynamicResourceHeuristicsFeature featureID = "dynamic_resource_heuristics"
)

// featureRegistry contains the registry of defined features.
Expand All @@ -43,16 +42,13 @@ const (
// 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).",
OpenTelemetryAttributesFeature: {
Description: "Enable OpenTelemetry attributes support (Service Identity, Resource Names, URL Templates).",
TrackingID: "b/467342602,b/467403185",
},
OpenTelemetryLoggingFeature: {
Description: "Enable OpenTelemetry logging support (Service Identity, Resource Names, URL Templates).",
TrackingID: "b/476980971",
},
OpenTelemetryMetricsFeature: {
Description: "Enable OpenTelemetry M1 metrics support (Duration, RPC Method, URL Template, Error Mapping).",
DynamicResourceHeuristicsFeature: {
Description: "Enable dynamic resource name heuristics for unannotated legacy services.",
TrackingID: "b/476980139",
},
MTLSHardBoundTokensFeature: {
Description: "support MTLS hard bound tokens",
Expand Down
11 changes: 11 additions & 0 deletions internal/gengapic/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ type generator struct {
// customOpServices is a map of service descriptors with methods that create custom operations
// to the service descriptors of the custom operation services that manage those custom operation instances.
customOpServices map[*descriptorpb.ServiceDescriptorProto]*descriptorpb.ServiceDescriptorProto

// learned vocabulary for heuristic path templates
vocabulary map[string]bool
}

func newGenerator(req *pluginpb.CodeGeneratorRequest) (*generator, error) {
Expand Down Expand Up @@ -102,6 +105,14 @@ func newGenerator(req *pluginpb.CodeGeneratorRequest) (*generator, error) {
// attach config to generator.
g.cfg = cfg

var methods []*descriptorpb.MethodDescriptorProto
for _, f := range req.GetProtoFile() {
for _, s := range f.GetService() {
methods = append(methods, s.GetMethod()...)
}
}
g.vocabulary = buildHeuristicVocabulary(methods)

files := req.GetProtoFile()
files = append(files, wellKnownTypeFiles...)

Expand Down
108 changes: 71 additions & 37 deletions internal/gengapic/gengapic.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,59 +498,73 @@ 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) || g.featureEnabled(OpenTelemetryLoggingFeature) {
resField := g.resourceNameField(m)
if resField != "" {
if g.featureEnabled(OpenTelemetryAttributesFeature) {
resTarget := g.resourceNameField(m)
if resTarget != nil {
p("if gax.IsFeatureEnabled(\"TRACING\") || gax.IsFeatureEnabled(\"LOGGING\") {")

// 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
var getters []string
for _, f := range resTarget.FieldNames {
getters = append(getters, fmt.Sprintf("req%s", fieldGetter(f)))
}
gettersStr := strings.Join(getters, ", ")

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

escapedFormat := strings.ReplaceAll(resTarget.Format, "%", "%%")
if host != "" {
p(` ctx = metadata.AppendToOutgoingContext(ctx, "gcp.resource.name", fmt.Sprintf("//%s/%%v", req%s))`, host, fieldGetter(resField))
p(` ctx = callctx.WithTelemetryContext(ctx, "resource_name", fmt.Sprintf("//%s/`+escapedFormat+`", `+gettersStr+`))`, host)
} else {
p(` ctx = metadata.AppendToOutgoingContext(ctx, "gcp.resource.name", fmt.Sprintf("%%v", req%s))`, fieldGetter(resField))
p(` ctx = callctx.WithTelemetryContext(ctx, "resource_name", fmt.Sprintf("` + escapedFormat + `", ` + gettersStr + `))`)
}
p("}")
g.imports[pbinfo.ImportSpec{Path: "google.golang.org/grpc/metadata"}] = true
g.imports[pbinfo.ImportSpec{Path: "github.com/googleapis/gax-go/v2/callctx"}] = 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) || g.featureEnabled(OpenTelemetryLoggingFeature) {
resField := g.resourceNameField(m)
if resField != "" {
if g.featureEnabled(OpenTelemetryAttributesFeature) {
resTarget := g.resourceNameField(m)
if resTarget != nil {
p("if gax.IsFeatureEnabled(\"TRACING\") || gax.IsFeatureEnabled(\"LOGGING\") {")
// 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
var getters []string
for _, f := range resTarget.FieldNames {
getters = append(getters, fmt.Sprintf("req%s", fieldGetter(f)))
}
gettersStr := strings.Join(getters, ", ")

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

escapedFormat := strings.ReplaceAll(resTarget.Format, "%", "%%")
if host != "" {
p(` ctx = metadata.AppendToOutgoingContext(ctx, "gcp.resource.name", fmt.Sprintf("//%s/%%v", req%s))`, host, fieldGetter(resField))
p(` ctx = callctx.WithTelemetryContext(ctx, "resource_name", fmt.Sprintf("//%s/`+escapedFormat+`", `+gettersStr+`))`, host)
} else {
p(` ctx = metadata.AppendToOutgoingContext(ctx, "gcp.resource.name", fmt.Sprintf("%%v", req%s))`, fieldGetter(resField))
p(` ctx = callctx.WithTelemetryContext(ctx, "resource_name", fmt.Sprintf("` + escapedFormat + `", ` + gettersStr + `))`)
}
p("}")
g.imports[pbinfo.ImportSpec{Path: "google.golang.org/grpc/metadata"}] = true
g.imports[pbinfo.ImportSpec{Path: "github.com/googleapis/gax-go/v2/callctx"}] = true
g.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
}
}
Expand Down Expand Up @@ -685,12 +699,14 @@ func (g *generator) injectTelemetryContext(m *descriptorpb.MethodDescriptorProto
if m == nil {
return
}
if g.featureEnabled(OpenTelemetryMetricsFeature) || g.featureEnabled(OpenTelemetryTracingFeature) {
if g.featureEnabled(OpenTelemetryAttributesFeature) {
g.imports[pbinfo.ImportSpec{Path: "github.com/googleapis/gax-go/v2/callctx"}] = true
g.imports[pbinfo.ImportSpec{Name: "gax", Path: "github.com/googleapis/gax-go/v2"}] = true

serv := g.descInfo.ParentElement[m].(*descriptorpb.ServiceDescriptorProto)
fqn := fmt.Sprintf("%s.%s/%s", g.descInfo.ParentFile[serv].GetPackage(), serv.GetName(), m.GetName())
g.printf("if gax.IsFeatureEnabled(\"METRICS\") || gax.IsFeatureEnabled(\"TRACING\") {")

g.printf("if gax.IsFeatureEnabled(\"METRICS\") || gax.IsFeatureEnabled(\"TRACING\") || gax.IsFeatureEnabled(\"LOGGING\") {")
g.printf(" ctx = callctx.WithTelemetryContext(ctx, \"rpc_method\", %q)", fqn)
if info != nil && info.url != "" {
g.printf(" ctx = callctx.WithTelemetryContext(ctx, \"url_template\", %q)", info.url)
Expand Down Expand Up @@ -891,17 +907,17 @@ func parseDynamicRequestHeaders(m *descriptorpb.MethodDescriptorProto) [][]strin
// 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 {
func (g *generator) resourceNameField(m *descriptorpb.MethodDescriptorProto) *heuristicTarget {
if m.GetInputType() == "" {
return ""
return nil
}
inType := g.descInfo.Type[m.GetInputType()]
if inType == nil {
return ""
return nil
}
msg, ok := inType.(*descriptorpb.DescriptorProto)
if !ok {
return ""
return nil
}

var candidates []string
Expand All @@ -912,29 +928,47 @@ func (g *generator) resourceNameField(m *descriptorpb.MethodDescriptorProto) str
}

if len(candidates) == 0 {
return ""
}
if len(candidates) == 1 {
return candidates[0]
if !g.featureEnabled(DynamicResourceHeuristicsFeature) {
return nil
}
if m.GetOptions() == nil {
return nil
}
eHTTP := proto.GetExtension(m.GetOptions(), annotations.E_Http)
if eHTTP == nil {
return nil
}
h, ok := eHTTP.(*annotations.HttpRule)
if !ok || h == nil {
return nil
}
target, err := identifyHeuristicTarget(m, h, g.vocabulary)
if err != nil || target == nil {
return nil
}
return target
}

// 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
selected := candidates[0]
if len(candidates) > 1 {
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
for _, c := range candidates {
if pathParams[c] {
selected = c
break
}
}
}

return candidates[0]
return &heuristicTarget{
Format: "%v",
FieldNames: []string{selected},
}
}
Loading
Loading