Summary
Gapic::LoggingConcerns.normalize_service looks up the Service constant with const_defined?'s default inherit: true. Generated REST stubs have no sibling Service constant, so the lookup walks up the ancestry to Object. In any application that defines a top-level Service constant, it finds that instead and calls service_name on it, raising NoMethodError while constructing the client.
gRPC transports are unaffected, because the gRPC layout does define a sibling Service.
Affected versions
Reproduced on gapic-common 1.1.0. The relevant code is identical in 1.2.0 and 1.3.0 (current latest), so I believe all three are affected.
Reproduction
No framework needed — a plain script and a top-level Service:
require "gapic/logging_concerns"
# Stand-in for an application class. Any top-level `Service` triggers this;
# it does not have to be a model or anything framework-specific.
class Service; end
# Stand-in for a generated REST client's namespace.
module Example
module V1
module ExampleService
module Rest
class ServiceStub; end
end
end
end
end
mod = Example::V1::ExampleService::Rest
puts "Service defined locally in #{mod}? #{mod.const_defined?('Service', false)}"
puts "Service visible via inherit: true? #{mod.const_defined?('Service')}"
puts "resolves to top-level ::Service? #{mod.const_get('Service').equal?(::Service)}"
Gapic::LoggingConcerns.normalize_service Example::V1::ExampleService::Rest::ServiceStub
Output:
Service defined locally in Example::V1::ExampleService::Rest? false
Service visible via inherit: true? true
resolves to top-level ::Service? true
NoMethodError: undefined method 'service_name' for class Service
gapic-common-1.1.0/lib/gapic/logging_concerns.rb:204:in 'Gapic::LoggingConcerns.normalize_service'
Expected
normalize_service falls through to the Rest branch and returns the dotted service name, as it does when no unrelated Service constant happens to be in scope.
Actual
NoMethodError: undefined method 'service_name' for class Service, raised during client construction.
Cause
lib/gapic/logging_concerns.rb (1.3.0, lines 197-211):
def normalize_service input
case input
when String
input
when Class
mod = input.name.split("::")[..-2].inject(Object) { |m, n| m.const_get n }
if mod.const_defined? "Service" # <- inherit: true
mod.const_get("Service").service_name
else
name_segments = input.name.split("::")[..-3]
mod = name_segments.inject(Object) { |m, n| m.const_get n }
name_segments.join "." if mod.const_defined? "Rest"
end
end
end
Suggested fix
Scope the lookup to the module itself:
if mod.const_defined? "Service", false
That keeps the gRPC path working (its Service is defined locally) and lets the REST path reach its intended fallback branch.
Notes
normalize_service runs before the logger is configured, so setting GOOGLE_SDK_RUBY_LOGGING_GEMS=false (or none) does not avoid it — the exception is raised whether or not logging is enabled.
This surfaced when switching a Secret Manager client from gRPC to REST; the only workaround I found was defining a Service constant inside each generated Rest namespace, which has to be repeated for every nested client the library constructs (a Secret Manager REST client also builds a Google::Cloud::Location::Locations::Rest::ServiceStub).
Summary
Gapic::LoggingConcerns.normalize_servicelooks up theServiceconstant withconst_defined?'s defaultinherit: true. Generated REST stubs have no siblingServiceconstant, so the lookup walks up the ancestry toObject. In any application that defines a top-levelServiceconstant, it finds that instead and callsservice_nameon it, raisingNoMethodErrorwhile constructing the client.gRPC transports are unaffected, because the gRPC layout does define a sibling
Service.Affected versions
Reproduced on
gapic-common1.1.0. The relevant code is identical in 1.2.0 and 1.3.0 (current latest), so I believe all three are affected.Reproduction
No framework needed — a plain script and a top-level
Service:Output:
Expected
normalize_servicefalls through to theRestbranch and returns the dotted service name, as it does when no unrelatedServiceconstant happens to be in scope.Actual
NoMethodError: undefined method 'service_name' for class Service, raised during client construction.Cause
lib/gapic/logging_concerns.rb(1.3.0, lines 197-211):Suggested fix
Scope the lookup to the module itself:
That keeps the gRPC path working (its
Serviceis defined locally) and lets the REST path reach its intended fallback branch.Notes
normalize_serviceruns before the logger is configured, so settingGOOGLE_SDK_RUBY_LOGGING_GEMS=false(ornone) does not avoid it — the exception is raised whether or not logging is enabled.This surfaced when switching a Secret Manager client from gRPC to REST; the only workaround I found was defining a
Serviceconstant inside each generatedRestnamespace, which has to be repeated for every nested client the library constructs (a Secret Manager REST client also builds aGoogle::Cloud::Location::Locations::Rest::ServiceStub).