gapic-common 0.20.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ # Copyright 2024 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "google/cloud/env"
16
+ require "google/logging/message"
17
+ require "google/logging/structured_formatter"
18
+
19
+ module Gapic
20
+ ##
21
+ # A mixin module that handles logging setup for a stub.
22
+ #
23
+ module LoggingConcerns
24
+ ##
25
+ # The logger for this object.
26
+ #
27
+ # @return [Logger]
28
+ #
29
+ attr_reader :logger
30
+
31
+ ##
32
+ # @private
33
+ # A convenience object used by stub-based logging.
34
+ #
35
+ class StubLogger
36
+ OMIT_FILES = [
37
+ /^#{Regexp.escape __dir__}/
38
+ ].freeze
39
+
40
+ def initialize logger: nil, **kwargs
41
+ @logger = logger
42
+ @kwargs = kwargs
43
+ end
44
+
45
+ def enabled?
46
+ !!@logger
47
+ end
48
+
49
+ def log severity
50
+ return unless @logger
51
+ locations = caller_locations
52
+ @logger.add severity do
53
+ builder = LogEntryBuilder.new(**@kwargs)
54
+ builder.set_source_location_from locations
55
+ yield builder
56
+ builder.build
57
+ rescue StandardError
58
+ # Do nothing
59
+ end
60
+ end
61
+
62
+ def info(&)
63
+ log(Logger::INFO, &)
64
+ end
65
+
66
+ def debug(&)
67
+ log(Logger::DEBUG, &)
68
+ end
69
+
70
+ ##
71
+ # @private
72
+ # Builder for a log entry, passed to {StubLogger#log}.
73
+ #
74
+ class LogEntryBuilder
75
+ def initialize system_name: nil,
76
+ service: nil,
77
+ endpoint: nil,
78
+ client_id: nil
79
+ @system_name = system_name
80
+ @service = service
81
+ @endpoint = endpoint
82
+ @client_id = client_id
83
+ @message = nil
84
+ @caller_locations = caller_locations
85
+ @fields = { "clientId" => @client_id }
86
+ end
87
+
88
+ attr_reader :system_name
89
+
90
+ attr_reader :service
91
+
92
+ attr_reader :endpoint
93
+
94
+ attr_accessor :message
95
+
96
+ attr_writer :source_location
97
+
98
+ attr_reader :fields
99
+
100
+ def set name, value
101
+ fields[name] = value
102
+ end
103
+
104
+ def set_system_name
105
+ set "system", system_name
106
+ end
107
+
108
+ def set_service
109
+ set "serviceName", service
110
+ end
111
+
112
+ def set_credentials_fields creds
113
+ creds = creds.client if creds.respond_to? :client
114
+ set "credentialsId", creds.object_id
115
+ set "credentialsType", creds.class.name
116
+ set "credentialsScope", creds.scope if creds.respond_to? :scope
117
+ set "useSelfSignedJWT", creds.enable_self_signed_jwt? if creds.respond_to? :enable_self_signed_jwt?
118
+ set "universeDomain", creds.universe_domain if creds.respond_to? :universe_domain
119
+ end
120
+
121
+ def source_location
122
+ @source_location ||= Google::Logging::SourceLocation.for_caller omit_files: OMIT_FILES,
123
+ locations: @caller_locations
124
+ end
125
+
126
+ def set_source_location_from locations
127
+ @caller_locations = locations
128
+ @source_location = nil
129
+ end
130
+
131
+ def build
132
+ Google::Logging::Message.from message: message, source_location: source_location, fields: fields
133
+ end
134
+ end
135
+ end
136
+
137
+ ##
138
+ # @private
139
+ # Initialize logging concerns.
140
+ #
141
+ def setup_logging logger: :default,
142
+ stream: nil,
143
+ formatter: nil,
144
+ level: nil,
145
+ system_name: nil,
146
+ service: nil,
147
+ endpoint: nil,
148
+ client_id: nil
149
+ service = LoggingConcerns.normalize_service service
150
+ system_name = LoggingConcerns.normalize_system_name system_name
151
+ logging_env = ENV["GOOGLE_SDK_RUBY_LOGGING_GEMS"].to_s.downcase
152
+ logger = nil if ["false", "none"].include? logging_env
153
+ if logger == :default
154
+ logger = nil
155
+ if ["true", "all"].include?(logging_env) || logging_env.split(",").include?(system_name)
156
+ stream ||= $stderr
157
+ level ||= "DEBUG"
158
+ formatter ||= Google::Logging::StructuredFormatter.new if Google::Cloud::Env.get.logging_agent_expected?
159
+ logger = Logger.new stream, progname: system_name, level: level, formatter: formatter
160
+ end
161
+ end
162
+ @logger = logger
163
+ @stub_logger = StubLogger.new logger: logger,
164
+ system_name: system_name,
165
+ service: service,
166
+ endpoint: endpoint,
167
+ client_id: client_id
168
+ end
169
+
170
+ # @private
171
+ attr_reader :stub_logger
172
+
173
+ class << self
174
+ # @private
175
+ def random_uuid4
176
+ ary = Random.bytes 16
177
+ ary.setbyte 6, ((ary.getbyte(6) & 0x0f) | 0x40)
178
+ ary.setbyte 8, ((ary.getbyte(8) & 0x3f) | 0x80)
179
+ ary.unpack("H8H4H4H4H12").join "-"
180
+ end
181
+
182
+ # @private
183
+ def normalize_system_name input
184
+ case input
185
+ when String
186
+ input
187
+ when Class
188
+ input.name.split("::")[..-3]
189
+ .map { |elem| elem.scan(/[A-Z][A-Z]*(?=[A-Z][a-z0-9]|$)|[A-Z][a-z0-9]+/).map(&:downcase).join("_") }
190
+ .join("-")
191
+ else
192
+ "googleapis"
193
+ end
194
+ end
195
+
196
+ # @private
197
+ def normalize_service input
198
+ case input
199
+ when String
200
+ input
201
+ when Class
202
+ mod = input.name.split("::")[..-2].inject(Object) { |m, n| m.const_get n }
203
+ if mod.const_defined? "Service"
204
+ mod.const_get("Service").service_name
205
+ else
206
+ name_segments = input.name.split("::")[..-3]
207
+ mod = name_segments.inject(Object) { |m, n| m.const_get n }
208
+ name_segments.join "." if mod.const_defined? "Rest"
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -12,80 +12,40 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "gapic/common/retry_policy"
16
+
15
17
  module Gapic
16
18
  class Operation
17
19
  ##
18
- # The policy for retrying operation reloads using an incremental backoff. A new object instance should be used for
19
- # every Operation invocation.
20
+ # The policy for retrying operation reloads using an incremental backoff.
21
+ #
22
+ # A new object instance should be used for every Operation invocation.
20
23
  #
21
- class RetryPolicy
24
+ class RetryPolicy < Gapic::Common::RetryPolicy
25
+ # @return [Numeric] Default initial delay in seconds.
26
+ DEFAULT_INITIAL_DELAY = 10
27
+ # @return [Numeric] Default maximum delay in seconds.
28
+ DEFAULT_MAX_DELAY = 300 # Five minutes
29
+ # @return [Numeric] Default delay scaling factor for subsequent retry attempts.
30
+ DEFAULT_MULTIPLIER = 1.3
31
+ # @return [Numeric] Default timeout threshold value in seconds.
32
+ DEFAULT_TIMEOUT = 3600 # One hour
33
+
22
34
  ##
23
35
  # Create new Operation RetryPolicy.
24
36
  #
25
- # @param initial_delay [Numeric] client-side timeout
26
- # @param multiplier [Numeric] client-side timeout
27
- # @param max_delay [Numeric] client-side timeout
28
- # @param timeout [Numeric] client-side timeout
37
+ # @param initial_delay [Numeric] Initial delay in seconds.
38
+ # @param multiplier [Numeric] The delay scaling factor for each subsequent retry attempt.
39
+ # @param max_delay [Numeric] Maximum delay in seconds.
40
+ # @param timeout [Numeric] Timeout threshold value in seconds.
29
41
  #
30
42
  def initialize initial_delay: nil, multiplier: nil, max_delay: nil, timeout: nil
31
- @initial_delay = initial_delay
32
- @multiplier = multiplier
33
- @max_delay = max_delay
34
- @timeout = timeout
35
- @delay = nil
36
- end
37
-
38
- def initial_delay
39
- @initial_delay || 10
40
- end
41
-
42
- def multiplier
43
- @multiplier || 1.3
44
- end
45
-
46
- def max_delay
47
- @max_delay || 300 # Five minutes
48
- end
49
-
50
- def timeout
51
- @timeout || 3600 # One hour
52
- end
53
-
54
- def call
55
- return unless retry?
56
-
57
- delay!
58
- increment_delay!
59
-
60
- true
61
- end
62
-
63
- private
64
-
65
- def deadline
66
- # memoize the deadline
67
- @deadline ||= Time.now + timeout
68
- end
69
-
70
- def retry?
71
- deadline > Time.now
72
- end
73
-
74
- ##
75
- # The current delay value.
76
- def delay
77
- @delay || initial_delay
78
- end
79
-
80
- def delay!
81
- # Call Kernel.sleep so we can stub it.
82
- Kernel.sleep delay
83
- end
84
-
85
- ##
86
- # Calculate and set the next delay value.
87
- def increment_delay!
88
- @delay = [delay * multiplier, max_delay].min
43
+ super(
44
+ initial_delay: initial_delay || DEFAULT_INITIAL_DELAY,
45
+ max_delay: max_delay || DEFAULT_MAX_DELAY,
46
+ multiplier: multiplier || DEFAULT_MULTIPLIER,
47
+ timeout: timeout || DEFAULT_TIMEOUT
48
+ )
89
49
  end
90
50
  end
91
51
  end
@@ -250,7 +250,7 @@ module Gapic
250
250
  # @return [Boolean]
251
251
  #
252
252
  def next_page_token?
253
- return if @response.nil?
253
+ return if @response.nil? # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
254
254
 
255
255
  !@response.next_page_token.empty?
256
256
  end
@@ -13,7 +13,9 @@
13
13
  # limitations under the License.
14
14
 
15
15
  require "googleauth"
16
+ require "gapic/logging_concerns"
16
17
  require "gapic/rest/faraday_middleware"
18
+ require "gapic/universe_domain_concerns"
17
19
  require "faraday/retry"
18
20
 
19
21
  module Gapic
@@ -26,35 +28,64 @@ module Gapic
26
28
  # - store credentials and add auth information to the request
27
29
  #
28
30
  class ClientStub
31
+ include UniverseDomainConcerns
32
+ include LoggingConcerns
33
+
29
34
  ##
30
35
  # Initializes with an endpoint and credentials
31
- # @param endpoint [String] an endpoint for the service that this stub will send requests to
36
+ #
37
+ # @param endpoint [String] The endpoint of the API. Overrides any endpoint_template.
38
+ # @param endpoint_template [String] The endpoint of the API, where the
39
+ # universe domain component of the hostname is marked by the string in
40
+ # the constant {UniverseDomainConcerns::ENDPOINT_SUBSTITUTION}.
41
+ # @param universe_domain [String] The universe domain in which calls
42
+ # should be made. Defaults to `googleapis.com`.
32
43
  # @param credentials [Google::Auth::Credentials]
33
44
  # Credentials to send with calls in form of a googleauth credentials object.
34
45
  # (see the [googleauth docs](https://googleapis.dev/ruby/googleauth/latest/index.html))
35
46
  # @param numeric_enums [Boolean] Whether to signal the server to JSON-encode enums as ints
36
- #
37
47
  # @param raise_faraday_errors [Boolean]
38
48
  # Whether to raise Faraday errors instead of wrapping them in `Gapic::Rest::Error`
39
49
  # Added for backwards compatibility.
40
50
  # Default is `true`. All REST clients (except for old versions of `google-cloud-compute-v1`)
41
51
  # should explicitly set this parameter to `false`.
52
+ # @param logger [Logger,:default,nil] An explicit logger to use, or one
53
+ # of the values `:default` (the default) to construct a default logger,
54
+ # or `nil` to disable logging explicitly.
42
55
  #
43
56
  # @yield [Faraday::Connection]
44
57
  #
45
- def initialize endpoint:, credentials:, numeric_enums: false, raise_faraday_errors: true
46
- @endpoint = endpoint
47
- @endpoint = "https://#{endpoint}" unless /^https?:/.match? endpoint
48
- @endpoint = @endpoint.sub %r{/$}, ""
58
+ def initialize credentials:,
59
+ endpoint: nil,
60
+ endpoint_template: nil,
61
+ universe_domain: nil,
62
+ numeric_enums: false,
63
+ raise_faraday_errors: true,
64
+ logging_system: nil,
65
+ service_name: nil,
66
+ logger: :default
67
+ setup_universe_domain universe_domain: universe_domain,
68
+ endpoint: endpoint,
69
+ endpoint_template: endpoint_template,
70
+ credentials: credentials
71
+
72
+ endpoint_url = self.endpoint
73
+ endpoint_url = "https://#{endpoint_url}" unless /^https?:/.match? endpoint_url
74
+ endpoint_url = endpoint_url.sub %r{/$}, ""
75
+
76
+ setup_logging logger: logger,
77
+ system_name: logging_system,
78
+ service: service_name,
79
+ endpoint: endpoint_url,
80
+ client_id: object_id
49
81
 
50
- @credentials = credentials
51
82
  @numeric_enums = numeric_enums
52
83
 
53
84
  @raise_faraday_errors = raise_faraday_errors
54
85
 
55
- @connection = Faraday.new url: @endpoint do |conn|
86
+ @connection = Faraday.new url: endpoint_url do |conn|
56
87
  conn.headers = { "Content-Type" => "application/json" }
57
- conn.request :google_authorization, @credentials unless @credentials.is_a? ::Symbol
88
+ conn.request :google_authorization, self.credentials unless self.credentials.is_a? ::Symbol
58
89
  conn.request :retry
59
90
  conn.response :raise_error
60
91
  conn.adapter :net_http
@@ -71,8 +102,8 @@ module Gapic
71
102
  # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
72
103
  # to the REST call. Currently only timeout and headers are supported.
73
104
  # @return [Faraday::Response]
74
- def make_get_request uri:, params: {}, options: {}
75
- make_http_request :get, uri: uri, body: nil, params: params, options: options
105
+ def make_get_request uri:, params: {}, options: {}, method_name: nil
106
+ make_http_request :get, uri: uri, body: nil, params: params, options: options, method_name: method_name
76
107
  end
77
108
 
78
109
  ##
@@ -83,8 +114,8 @@ module Gapic
83
114
  # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
84
115
  # to the REST call. Currently only timeout and headers are supported.
85
116
  # @return [Faraday::Response]
86
- def make_delete_request uri:, params: {}, options: {}
87
- make_http_request :delete, uri: uri, body: nil, params: params, options: options
117
+ def make_delete_request uri:, params: {}, options: {}, method_name: nil
118
+ make_http_request :delete, uri: uri, body: nil, params: params, options: options, method_name: method_name
88
119
  end
89
120
 
90
121
  ##
@@ -96,8 +127,8 @@ module Gapic
96
127
  # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
97
128
  # to the REST call. Currently only timeout and headers are supported.
98
129
  # @return [Faraday::Response]
99
- def make_patch_request uri:, body:, params: {}, options: {}
100
- make_http_request :patch, uri: uri, body: body, params: params, options: options
130
+ def make_patch_request uri:, body:, params: {}, options: {}, method_name: nil
131
+ make_http_request :patch, uri: uri, body: body, params: params, options: options, method_name: method_name
101
132
  end
102
133
 
103
134
  ##
@@ -109,8 +140,8 @@ module Gapic
109
140
  # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
110
141
  # to the REST call. Currently only timeout and headers are supported.
111
142
  # @return [Faraday::Response]
112
- def make_post_request uri:, body: nil, params: {}, options: {}
113
- make_http_request :post, uri: uri, body: body, params: params, options: options
143
+ def make_post_request uri:, body: nil, params: {}, options: {}, method_name: nil
144
+ make_http_request :post, uri: uri, body: body, params: params, options: options, method_name: method_name
114
145
  end
115
146
 
116
147
  ##
@@ -122,8 +153,8 @@ module Gapic
122
153
  # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
123
154
  # to the REST call. Currently only timeout and headers are supported.
124
155
  # @return [Faraday::Response]
125
- def make_put_request uri:, body: nil, params: {}, options: {}
126
- make_http_request :put, uri: uri, body: body, params: params, options: options
156
+ def make_put_request uri:, body: nil, params: {}, options: {}, method_name: nil
157
+ make_http_request :put, uri: uri, body: body, params: params, options: options, method_name: method_name
127
158
  end
128
159
 
129
160
  ##
@@ -137,27 +168,38 @@ module Gapic
137
168
  # @param is_server_streaming [Boolean] flag if method is streaming
138
169
  # @yieldparam chunk [String] The chunk of data received during server streaming.
139
170
  # @return [Faraday::Response]
140
- def make_http_request verb, uri:, body:, params:, options:, is_server_streaming: false, &block
171
+ def make_http_request verb,
172
+ uri:, body:, params:, options:,
173
+ is_server_streaming: false, method_name: nil,
174
+ &block
141
175
  # Converts hash and nil to an options object
142
176
  options = ::Gapic::CallOptions.new(**options.to_h) unless options.is_a? ::Gapic::CallOptions
143
177
  deadline = calculate_deadline options
144
178
  retried_exception = nil
145
179
  next_timeout = get_timeout deadline
180
+ request_id = LoggingConcerns.random_uuid4
181
+ try_number = 1
146
182
 
147
183
  begin
148
- base_make_http_request(verb: verb, uri: uri, body: body,
149
- params: params, metadata: options.metadata,
150
- timeout: next_timeout,
151
- is_server_streaming: is_server_streaming,
152
- &block)
184
+ log_request method_name, request_id, try_number, body, options.metadata
185
+ response = base_make_http_request verb: verb, uri: uri, body: body,
186
+ params: params, metadata: options.metadata,
187
+ timeout: next_timeout,
188
+ is_server_streaming: is_server_streaming,
189
+ &block
190
+ log_response method_name, request_id, try_number, response, is_server_streaming
191
+ response
153
192
  rescue ::Faraday::TimeoutError => e
193
+ log_response method_name, request_id, try_number, e, is_server_streaming
154
194
  raise if @raise_faraday_errors
155
195
  raise Gapic::Rest::DeadlineExceededError.wrap_faraday_error e, root_cause: retried_exception
156
196
  rescue ::Faraday::Error => e
197
+ log_response method_name, request_id, try_number, e, is_server_streaming
157
198
  next_timeout = get_timeout deadline
158
199
 
159
200
  if check_retry?(next_timeout) && options.retry_policy.call(e)
160
201
  retried_exception = e
202
+ try_number += 1
161
203
  retry
162
204
  end
163
205
 
@@ -234,6 +276,54 @@ module Gapic
234
276
 
235
277
  timeout.positive?
236
278
  end
279
+
280
+ def log_request method_name, request_id, try_number, body, metadata
281
+ return unless stub_logger&.enabled?
282
+ stub_logger.info do |entry|
283
+ entry.set_system_name
284
+ entry.set_service
285
+ entry.set "rpcName", method_name
286
+ entry.set "retryAttempt", try_number
287
+ entry.set "requestId", request_id
288
+ entry.message = "Sending request to #{entry.service}.#{method_name} (try #{try_number})"
289
+ end
290
+ body = body.to_s
291
+ metadata = metadata.to_h rescue {}
292
+ return if body.empty? && metadata.empty?
293
+ stub_logger.debug do |entry|
294
+ entry.set "requestId", request_id
295
+ entry.set "request", body
296
+ entry.set "headers", metadata
297
+ entry.message = "(request payload as JSON)"
298
+ end
299
+ end
300
+
301
+ def log_response method_name, request_id, try_number, response, is_server_streaming
302
+ return unless stub_logger&.enabled?
303
+ stub_logger.info do |entry|
304
+ entry.set_system_name
305
+ entry.set_service
306
+ entry.set "rpcName", method_name
307
+ entry.set "retryAttempt", try_number
308
+ entry.set "requestId", request_id
309
+ if response.is_a? StandardError
310
+ entry.set "exception", response.to_s
311
+ entry.message = "Received error for #{entry.service}.#{method_name} (try #{try_number}): #{response}"
312
+ elsif is_server_streaming
313
+ entry.message = "Receiving stream for #{entry.service}.#{method_name} (try #{try_number})"
314
+ else
315
+ entry.message = "Received response for #{entry.service}.#{method_name} (try #{try_number})"
316
+ end
317
+ end
318
+ return if is_server_streaming || !response.respond_to?(:body)
319
+ body = response.body.to_s
320
+ return if body.empty?
321
+ stub_logger.debug do |entry|
322
+ entry.set "requestId", request_id
323
+ entry.set "response", body
324
+ entry.message = "(response payload as JSON)"
325
+ end
326
+ end
237
327
  end
238
328
  end
239
329
  end
@@ -0,0 +1,48 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "gapic/config"
16
+
17
+ module Gapic
18
+ module Rest
19
+ ##
20
+ # @private
21
+ # A specialized Configuration class to use when generating
22
+ # package-level binding override configuration.
23
+ #
24
+ # This configuration is for internal use of the client library classes,
25
+ # and it is not intended that the end-users will read or change it.
26
+ #
27
+ # @attribute [rw] bindings_override
28
+ # Overrides for http bindings for the RPC of the mixins for the given package.
29
+ # return [::Hash{::Symbol=>::Array<::Gapic::Rest::GrpcTranscoder::HttpBinding>}]
30
+ #
31
+ class HttpBindingOverrideConfiguration
32
+ extend ::Gapic::Config
33
+
34
+ # @private
35
+ # Overrides for http bindings for the RPC of the mixins for the given package.
36
+ # Services in the package should use these when creating clients for the mixin services.
37
+ # @return [::Hash{::Symbol=>::Array<::Gapic::Rest::GrpcTranscoder::HttpBinding>}]
38
+ config_attr :bindings_override, {}, ::Hash, nil
39
+
40
+ # @private
41
+ def initialize parent_config = nil
42
+ @parent_config = parent_config unless parent_config.nil?
43
+
44
+ yield self if block_given?
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/gapic/rest.rb CHANGED
@@ -25,6 +25,7 @@ require "gapic/rest/client_stub"
25
25
  require "gapic/rest/error"
26
26
  require "gapic/rest/faraday_middleware"
27
27
  require "gapic/rest/grpc_transcoder"
28
+ require "gapic/rest/http_binding_override_configuration"
28
29
  require "gapic/rest/operation"
29
30
  require "gapic/rest/paged_enumerable"
30
31
  require "gapic/rest/server_stream"