getstream-ruby 9.0.0 → 10.0.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.
- checksums.yaml +4 -4
- data/README.md +36 -0
- data/lib/getstream_ruby/client.rb +92 -31
- data/lib/getstream_ruby/configuration.rb +26 -29
- data/lib/getstream_ruby/log_redaction.rb +58 -0
- data/lib/getstream_ruby/request_logging.rb +97 -0
- data/lib/getstream_ruby/retry_config.rb +27 -0
- data/lib/getstream_ruby/version.rb +1 -1
- data/lib/getstream_ruby.rb +2 -0
- metadata +6 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7c2582f037fa1be220f8b72647c0a87254d0eeb42af2b5046cdb410852f25d7
|
|
4
|
+
data.tar.gz: a880f9c77e414fe74ee4dd7d82631c927b5af8b4c8b0dca6fca5d00386ff08cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10ad2c2ba0fb122b5a0048136d605eba7de662c86c7cd62123b8e84ad17f96a88750e0dc416764208e42106dab904d5127f711b36aab9083d637e8684582dd26
|
|
7
|
+
data.tar.gz: b1bffe4b14da26fb2761eb596c78b3337f8bd7f4600869e1be0792b4c581bea79108990bd7ab5898c083826b66a8b68a054eda8c2e4d93c94d75a9007735af67
|
data/README.md
CHANGED
|
@@ -163,6 +163,42 @@ rescue GetStreamRuby::APIError => e
|
|
|
163
163
|
end
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
## Retries
|
|
167
|
+
|
|
168
|
+
Retries are opt-in via `retry_config:` and disabled by default (the client makes exactly one attempt; errors surface unchanged). When enabled, only `GET`/`HEAD` requests are retried, and only when they fail with HTTP 429 (unless the error is marked `unrecoverable`) or a transport-level error (connection reset, timeout, DNS failure, TLS handshake failure). Writes, 5xx responses, and other 4xx responses are never retried.
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
client = GetStreamRuby::Client.new(
|
|
172
|
+
api_key: '...', api_secret: '...',
|
|
173
|
+
retry_config: GetStreamRuby::RetryConfig.new(enabled: true, max_attempts: 3, max_backoff: 30.0)
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`max_attempts` caps the total number of attempts (default 3). `max_backoff` caps the wait between attempts (default 30.0 seconds). A 429 with a `Retry-After` header waits exactly that long, clamped to `max_backoff`, with no jitter. Otherwise the wait is full jitter: a random delay between 0 and `min(max_backoff, 2**attempt)` seconds.
|
|
178
|
+
|
|
179
|
+
## Logging
|
|
180
|
+
|
|
181
|
+
Pass a stdlib `Logger` via `logger:` to get structured, single-line log events. With no `logger:`, the SDK produces zero output; the SDK never sets the logger's level either, that's the caller's call.
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
require 'logger'
|
|
185
|
+
|
|
186
|
+
client = GetStreamRuby.manual(
|
|
187
|
+
api_key: "your_api_key",
|
|
188
|
+
api_secret: "your_api_secret",
|
|
189
|
+
logger: Logger.new($stdout)
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Four events are emitted:
|
|
194
|
+
|
|
195
|
+
- `client.initialized` (INFO, once at construction): SDK version and the effective client config (pool size, timeouts, gzip, whether a custom `http_client`/`log_bodies` is set).
|
|
196
|
+
- `http.request.sent` (DEBUG, before each request).
|
|
197
|
+
- `http.response.received` (DEBUG, after any response including 4xx/5xx — those are just data, not a failure).
|
|
198
|
+
- `http.request.failed` (ERROR, transport failure only: no HTTP response was received at all, e.g. connection reset, timeout, DNS failure, TLS handshake failure). When [retries](#retries) are enabled, each retried attempt also emits `http.request.failed` at DEBUG with a `retry.attempt` field (1-indexed) before its backoff sleep; a 429 retry omits `error.type` (the paired `http.response.received` already recorded the status), a transport-error retry includes it.
|
|
199
|
+
|
|
200
|
+
Query values for `api_key`/`api_secret`/`token` are always redacted to `<redacted>`, and the same keys are redacted (shallowly, top-level only) in JSON body logging. No headers are ever logged. Request/response bodies are not logged by default; pass `log_bodies: true` to opt in (values for the keys above are still redacted). Enabling it emits one WARN at construction as a reminder that your logs will now contain body content.
|
|
201
|
+
|
|
166
202
|
## Development
|
|
167
203
|
|
|
168
204
|
### Quick Start
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'faraday'
|
|
4
4
|
require 'faraday/gzip'
|
|
5
|
-
require 'faraday/retry'
|
|
6
5
|
require 'faraday/multipart'
|
|
7
6
|
require 'faraday/net_http_persistent'
|
|
8
7
|
require 'logger'
|
|
@@ -20,11 +19,15 @@ require_relative 'generated/webhook'
|
|
|
20
19
|
require_relative 'generated/models/api_error'
|
|
21
20
|
require_relative 'stream_response'
|
|
22
21
|
require_relative 'error_mapping'
|
|
22
|
+
require_relative 'log_redaction'
|
|
23
|
+
require_relative 'request_logging'
|
|
23
24
|
|
|
24
25
|
module GetStreamRuby
|
|
25
26
|
|
|
26
27
|
class Client
|
|
27
28
|
|
|
29
|
+
include RequestLogging
|
|
30
|
+
|
|
28
31
|
# Backdate the JWT `iat` claim by this many seconds.
|
|
29
32
|
#
|
|
30
33
|
# JWT timestamps are whole-second (RFC 7519 NumericDate), so `Time.now.to_i`
|
|
@@ -38,20 +41,23 @@ module GetStreamRuby
|
|
|
38
41
|
attr_reader :configuration
|
|
39
42
|
|
|
40
43
|
def initialize(config = nil, api_key: nil, api_secret: nil, **options)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
if api_key || api_secret || !options.empty?
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
# Overrides win over an explicit config, matching prior behavior. Only
|
|
45
|
+
# fall back to GetStreamRuby.configuration when neither is given, so
|
|
46
|
+
# that path (currently unimplemented) isn't evaluated needlessly.
|
|
47
|
+
@configuration = if api_key || api_secret || !options.empty?
|
|
48
|
+
Configuration.with_overrides(
|
|
49
|
+
api_key: api_key,
|
|
50
|
+
api_secret: api_secret,
|
|
51
|
+
**options,
|
|
52
|
+
)
|
|
53
|
+
else
|
|
54
|
+
config || GetStreamRuby.configuration
|
|
55
|
+
end
|
|
51
56
|
|
|
52
57
|
@configuration.validate!
|
|
53
58
|
@connection = build_connection
|
|
54
59
|
@configuration.log_pool_config_to(@configuration.logger)
|
|
60
|
+
warn_log_bodies_enabled
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
def feed_resource
|
|
@@ -209,21 +215,76 @@ module GetStreamRuby
|
|
|
209
215
|
# Check if this is a file upload request that needs multipart
|
|
210
216
|
return make_multipart_request(method, path, query_params, data) if multipart_request?(data)
|
|
211
217
|
|
|
212
|
-
|
|
218
|
+
body_json = data.to_json
|
|
219
|
+
attempt = 0
|
|
213
220
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
begin
|
|
222
|
+
started = monotonic_now
|
|
223
|
+
log_request_sent(method, path, query_params, body_json)
|
|
224
|
+
|
|
225
|
+
response = @connection.send(method) do |req|
|
|
226
|
+
|
|
227
|
+
req.url path, query_params
|
|
228
|
+
req.headers['Authorization'] = generate_auth_header
|
|
229
|
+
req.headers['Content-Type'] = 'application/json'
|
|
230
|
+
req.headers['stream-auth-type'] = 'jwt'
|
|
231
|
+
req.headers['X-Stream-Client'] = user_agent
|
|
232
|
+
req.body = body_json
|
|
233
|
+
req.options.timeout = request_timeout if request_timeout
|
|
234
|
+
|
|
235
|
+
end
|
|
221
236
|
|
|
237
|
+
log_response_received(response, started)
|
|
238
|
+
handle_response(response)
|
|
239
|
+
rescue Faraday::Error => e
|
|
240
|
+
error = TransportError.new("Request failed: #{e.message}", error_type: ErrorMapping.classify_faraday_error(e))
|
|
241
|
+
if retry_eligible?(method, error, attempt)
|
|
242
|
+
wait_before_retry(method, path, error, attempt, started)
|
|
243
|
+
attempt += 1
|
|
244
|
+
retry
|
|
245
|
+
end
|
|
246
|
+
log_request_failed(method, path, e, started)
|
|
247
|
+
raise error
|
|
248
|
+
rescue RateLimitError => e
|
|
249
|
+
if retry_eligible?(method, e, attempt)
|
|
250
|
+
wait_before_retry(method, path, e, attempt, started)
|
|
251
|
+
attempt += 1
|
|
252
|
+
retry
|
|
253
|
+
end
|
|
254
|
+
raise
|
|
222
255
|
end
|
|
256
|
+
end
|
|
223
257
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
258
|
+
# True when the opt-in retry policy (GetStreamRuby::RetryConfig) allows
|
|
259
|
+
# another attempt for this failure: disabled by default (single attempt,
|
|
260
|
+
# errors surface unchanged); enabled only retries idempotent GET/HEAD
|
|
261
|
+
# requests that failed with a recoverable RateLimitError or a
|
|
262
|
+
# TransportError, and only while attempts remain.
|
|
263
|
+
def retry_eligible?(method, error, attempt)
|
|
264
|
+
config = @configuration.retry_config
|
|
265
|
+
return false unless config&.enabled?
|
|
266
|
+
return false unless %i[get head].include?(method)
|
|
267
|
+
return false if error.is_a?(RateLimitError) && error.unrecoverable
|
|
268
|
+
return false unless attempt + 1 < config.max_attempts
|
|
269
|
+
|
|
270
|
+
true
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Emits the retry-attempt log, then blocks for the backoff delay:
|
|
274
|
+
# RateLimitError#retry_after (if positive) capped at max_backoff, no
|
|
275
|
+
# jitter; otherwise full jitter over 2**attempt capped at max_backoff.
|
|
276
|
+
# `sleep` is called on self so specs can stub it.
|
|
277
|
+
def wait_before_retry(method, path, error, attempt, started)
|
|
278
|
+
log_retry_attempt(method, path, error, attempt, started)
|
|
279
|
+
config = @configuration.retry_config
|
|
280
|
+
delay =
|
|
281
|
+
if error.is_a?(RateLimitError) && error.retry_after&.positive?
|
|
282
|
+
[error.retry_after, config.max_backoff].min
|
|
283
|
+
else
|
|
284
|
+
ceiling = [config.max_backoff, 2.0**attempt].min
|
|
285
|
+
ceiling <= 0 ? 0.0 : rand * ceiling
|
|
286
|
+
end
|
|
287
|
+
sleep(delay)
|
|
227
288
|
end
|
|
228
289
|
|
|
229
290
|
def build_connection
|
|
@@ -234,13 +295,10 @@ module GetStreamRuby
|
|
|
234
295
|
Faraday.new(url: @configuration.base_url) do |conn|
|
|
235
296
|
|
|
236
297
|
conn.request :multipart
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
backoff_factor: 2,
|
|
242
|
-
}
|
|
243
|
-
conn.response :json, content_type: /\bjson$/
|
|
298
|
+
# preserve_raw: true keeps the wire string in env[:raw_body] alongside
|
|
299
|
+
# the parsed body, so response logging can report size/content
|
|
300
|
+
# without re-serializing an already-parsed Hash.
|
|
301
|
+
conn.response :json, content_type: /\bjson$/, preserve_raw: true
|
|
244
302
|
# :gzip must come after :json (Faraday runs response middleware in reverse).
|
|
245
303
|
conn.request :gzip
|
|
246
304
|
configure_adapter(conn)
|
|
@@ -277,8 +335,6 @@ module GetStreamRuby
|
|
|
277
335
|
# A fallback silently disables pooling, so always WARN (never swallow).
|
|
278
336
|
@configuration.warn_pool_fallback(Faraday.default_adapter, e)
|
|
279
337
|
connection.adapter Faraday.default_adapter
|
|
280
|
-
# Record the adapter actually built so the INFO log reports it accurately.
|
|
281
|
-
@configuration.effective_adapter = Faraday.default_adapter.to_s
|
|
282
338
|
end
|
|
283
339
|
|
|
284
340
|
def generate_auth_header
|
|
@@ -346,6 +402,9 @@ module GetStreamRuby
|
|
|
346
402
|
payload[:upload_sizes] = upload_sizes_json
|
|
347
403
|
end
|
|
348
404
|
|
|
405
|
+
started = monotonic_now
|
|
406
|
+
log_request_sent(method, path, query_params)
|
|
407
|
+
|
|
349
408
|
response = @connection.send(method) do |req|
|
|
350
409
|
|
|
351
410
|
req.url path, query_params
|
|
@@ -356,8 +415,10 @@ module GetStreamRuby
|
|
|
356
415
|
|
|
357
416
|
end
|
|
358
417
|
|
|
418
|
+
log_response_received(response, started)
|
|
359
419
|
handle_response(response)
|
|
360
420
|
rescue Faraday::Error => e
|
|
421
|
+
log_request_failed(method, path, e, started)
|
|
361
422
|
raise TransportError.new("Request failed: #{e.message}", error_type: ErrorMapping.classify_faraday_error(e))
|
|
362
423
|
end
|
|
363
424
|
|
|
@@ -8,7 +8,7 @@ module GetStreamRuby
|
|
|
8
8
|
|
|
9
9
|
attr_accessor :api_key, :api_secret, :base_url, :timeout, :logger, :faraday_adapter, :faraday_adapter_options,
|
|
10
10
|
:connection_keep_alive, :max_conns_per_host, :idle_timeout, :connect_timeout,
|
|
11
|
-
:request_timeout, :http_client, :
|
|
11
|
+
:request_timeout, :http_client, :log_bodies, :retry_config
|
|
12
12
|
|
|
13
13
|
def initialize(api_key: nil, api_secret: nil, use_env: true, **options)
|
|
14
14
|
http_options = options[:http_options] || {}
|
|
@@ -22,6 +22,8 @@ module GetStreamRuby
|
|
|
22
22
|
@timeout = @request_timeout
|
|
23
23
|
@http_client = options[:http_client]
|
|
24
24
|
@logger = options[:logger]
|
|
25
|
+
@log_bodies = options[:log_bodies] || false
|
|
26
|
+
@retry_config = options[:retry_config] || RetryConfig.new
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def valid?
|
|
@@ -48,41 +50,36 @@ module GetStreamRuby
|
|
|
48
50
|
faraday_adapter_options: @faraday_adapter_options.dup,
|
|
49
51
|
connection_keep_alive: @connection_keep_alive,
|
|
50
52
|
logger: @logger,
|
|
53
|
+
log_bodies: @log_bodies,
|
|
54
|
+
retry_config: @retry_config,
|
|
51
55
|
)
|
|
52
56
|
end
|
|
53
57
|
|
|
54
|
-
# Emit
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# to the default adapter is never misreported as the requested adapter.
|
|
58
|
+
# Emit the `client.initialized` INFO event once at construction (structured
|
|
59
|
+
# logging spec §6.1). A no-op when no logger is configured: unlike
|
|
60
|
+
# `warn_pool_fallback` below, a quiet start-up here is expected, not a
|
|
61
|
+
# silently-swallowed problem, so there is no $stdout fallback.
|
|
59
62
|
def log_pool_config_to(logger)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
logger.info(
|
|
74
|
-
format(
|
|
75
|
-
fmt,
|
|
76
|
-
m: @max_conns_per_host, i: @idle_timeout, c: @connect_timeout,
|
|
77
|
-
r: @request_timeout, flag: flag, a: adapter_label
|
|
78
|
-
),
|
|
79
|
-
)
|
|
63
|
+
return if logger.nil?
|
|
64
|
+
|
|
65
|
+
fields = {
|
|
66
|
+
'stream.sdk.name' => 'getstream-ruby',
|
|
67
|
+
'stream.sdk.version' => VERSION,
|
|
68
|
+
'stream.client.max_conns_per_host' => @max_conns_per_host,
|
|
69
|
+
'stream.client.idle_timeout_seconds' => @idle_timeout,
|
|
70
|
+
'stream.client.connect_timeout_seconds' => @connect_timeout,
|
|
71
|
+
'stream.client.request_timeout_seconds' => @request_timeout,
|
|
72
|
+
'stream.client.gzip_enabled' => true,
|
|
73
|
+
'stream.client.user_http_client' => !@http_client.nil?,
|
|
74
|
+
'stream.client.log_bodies' => @log_bodies,
|
|
75
|
+
}
|
|
76
|
+
logger.info { "client.initialized #{fields.map { |k, v| "#{k}=#{v}" }.join(' ')}" }
|
|
80
77
|
end
|
|
81
78
|
|
|
82
79
|
# Emit a WARNING that the requested adapter could not be built and pooling
|
|
83
|
-
# is disabled (CHA-2956). A fallback must never be silent, so
|
|
84
|
-
#
|
|
85
|
-
#
|
|
80
|
+
# is disabled (CHA-2956). A fallback must never be silent, so unlike the
|
|
81
|
+
# structured-logging events below, this uses a default $stdout logger when
|
|
82
|
+
# none is configured.
|
|
86
83
|
def warn_pool_fallback(fallback_adapter, error)
|
|
87
84
|
warn_logger = @logger || Logger.new($stdout).tap { |l| l.level = Logger::WARN }
|
|
88
85
|
warn_logger.warn(
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module GetStreamRuby
|
|
6
|
+
|
|
7
|
+
# Redaction helpers for the SDK's structured log events. Shallow by design.
|
|
8
|
+
module LogRedaction
|
|
9
|
+
|
|
10
|
+
REDACTED = '<redacted>'
|
|
11
|
+
QUERY_PARAMS = %w[api_key api_secret token].freeze
|
|
12
|
+
BODY_KEYS = %w[api_secret token password].freeze
|
|
13
|
+
# Matches `key=value` for the secret query params wherever they appear in a
|
|
14
|
+
# free-form string (e.g. a transport error message that embeds the request
|
|
15
|
+
# URL), value runs up to the next `&`, whitespace, or end of string.
|
|
16
|
+
MESSAGE_SECRET_PATTERN = /\b(#{QUERY_PARAMS.join('|')})=[^&\s]*/i.freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def redact_query(params)
|
|
21
|
+
return params if params.nil? || params.empty?
|
|
22
|
+
|
|
23
|
+
params.to_h { |k, v| [k, QUERY_PARAMS.include?(k.to_s.downcase) ? REDACTED : v] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def redact_json_body(body)
|
|
27
|
+
return body if body.nil? || body.empty?
|
|
28
|
+
|
|
29
|
+
data = JSON.parse(body)
|
|
30
|
+
return body unless data.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
changed = false
|
|
33
|
+
BODY_KEYS.each do |key|
|
|
34
|
+
|
|
35
|
+
if data.key?(key)
|
|
36
|
+
data[key] = REDACTED
|
|
37
|
+
changed = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
changed ? JSON.generate(data) : body
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
body
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Redacts secret query-parameter values wherever they appear in a free
|
|
47
|
+
# string (e.g. `error.message` from a transport exception, which may embed
|
|
48
|
+
# the full request URL). Case-insensitive on the key; the value is
|
|
49
|
+
# replaced regardless of its own case.
|
|
50
|
+
def redact_message(string)
|
|
51
|
+
return string if string.nil? || string.empty?
|
|
52
|
+
|
|
53
|
+
string.gsub(MESSAGE_SECRET_PATTERN) { "#{Regexp.last_match(1)}=#{REDACTED}" }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GetStreamRuby
|
|
4
|
+
|
|
5
|
+
# Structured-logging event emission mixed into Client (kept in its own file
|
|
6
|
+
# to stay under Metrics/ClassLength; these methods rely on Client's
|
|
7
|
+
# @configuration/monotonic_now same as if they lived on the class directly).
|
|
8
|
+
module RequestLogging
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# One-shot WARN at construction so a log_bodies: true deployment can't miss
|
|
13
|
+
# that bodies (secrets key-redacted, but otherwise verbatim) are now being
|
|
14
|
+
# written to its logs.
|
|
15
|
+
def warn_log_bodies_enabled
|
|
16
|
+
return unless @configuration.log_bodies && @configuration.logger
|
|
17
|
+
|
|
18
|
+
@configuration.logger.warn do
|
|
19
|
+
|
|
20
|
+
'HTTP request/response bodies will be logged. Auth headers and ' \
|
|
21
|
+
'known-secret fields are still redacted, but other sensitive ' \
|
|
22
|
+
'data (messages, PII) may appear in logs. Disable for production.'
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def log_request_sent(method, path, query_params, body_json = nil)
|
|
28
|
+
logger = @configuration.logger
|
|
29
|
+
return unless logger
|
|
30
|
+
|
|
31
|
+
query = LogRedaction.redact_query(query_params).map { |k, v| "#{k}=#{v}" }.join('&')
|
|
32
|
+
line = +"http.request.sent http.request.method=#{method} url.path=#{path} url.query=#{query}"
|
|
33
|
+
line << " http.request.body=#{LogRedaction.redact_json_body(body_json)}" if @configuration.log_bodies && body_json
|
|
34
|
+
logger.debug { line }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def log_response_received(response, started)
|
|
38
|
+
logger = @configuration.logger
|
|
39
|
+
return unless logger
|
|
40
|
+
|
|
41
|
+
raw_body = raw_response_body(response)
|
|
42
|
+
line = +"http.response.received http.response.status_code=#{response.status} " \
|
|
43
|
+
"http.response.body.size=#{raw_body.bytesize} duration_ms=#{elapsed_ms(started)}"
|
|
44
|
+
line << " http.response.body=#{LogRedaction.redact_json_body(raw_body)}" if @configuration.log_bodies
|
|
45
|
+
logger.debug { line }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def log_request_failed(method, path, error, started)
|
|
49
|
+
logger = @configuration.logger
|
|
50
|
+
return unless logger
|
|
51
|
+
|
|
52
|
+
error_type = ErrorMapping.classify_faraday_error(error)
|
|
53
|
+
message = LogRedaction.redact_message(error.message)
|
|
54
|
+
logger.error do
|
|
55
|
+
|
|
56
|
+
"http.request.failed http.request.method=#{method} url.path=#{path} " \
|
|
57
|
+
"error.type=#{error_type} error.message=#{message} duration_ms=#{elapsed_ms(started)}"
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# DEBUG-level counterpart to `log_request_failed`, emitted before each
|
|
63
|
+
# retry backoff sleep (opt-in RetryConfig). `error.type` is the closed
|
|
64
|
+
# transport-classifier enum (see ErrorMapping.classify_faraday_error /
|
|
65
|
+
# TRANSPORT_ERROR_TYPES) and is only meaningful for TransportError; a 429
|
|
66
|
+
# retry omits it rather than invent a value; the paired
|
|
67
|
+
# http.response.received log already recorded the 429 status.
|
|
68
|
+
def log_retry_attempt(method, path, error, attempt, started)
|
|
69
|
+
logger = @configuration.logger
|
|
70
|
+
return unless logger
|
|
71
|
+
|
|
72
|
+
message = LogRedaction.redact_message(error.message)
|
|
73
|
+
line = +"http.request.failed http.request.method=#{method} url.path=#{path} retry.attempt=#{attempt + 1}"
|
|
74
|
+
line << " error.type=#{error.error_type}" if error.is_a?(TransportError)
|
|
75
|
+
line << " error.message=#{message} duration_ms=#{elapsed_ms(started)}"
|
|
76
|
+
logger.debug { line }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def elapsed_ms(started)
|
|
80
|
+
((monotonic_now - started) * 1000).round
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The `:json` response middleware parses env[:body] in place once its
|
|
84
|
+
# content type matches, so the raw wire string only survives via
|
|
85
|
+
# `preserve_raw:` (see Client#build_connection). Falls back to
|
|
86
|
+
# `response.body` itself when it was never parsed (non-JSON content type,
|
|
87
|
+
# or no body).
|
|
88
|
+
def raw_response_body(response)
|
|
89
|
+
raw = response.env[:raw_body]
|
|
90
|
+
return raw unless raw.nil?
|
|
91
|
+
|
|
92
|
+
response.body.to_s
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GetStreamRuby
|
|
4
|
+
|
|
5
|
+
# Opt-in auto-retry policy. Disabled by default: the client performs exactly
|
|
6
|
+
# one attempt and surfaces errors unchanged. When enabled, only GET/HEAD
|
|
7
|
+
# requests failing with HTTP 429 or a transport error are retried.
|
|
8
|
+
class RetryConfig
|
|
9
|
+
|
|
10
|
+
attr_reader :max_attempts, :max_backoff
|
|
11
|
+
|
|
12
|
+
def initialize(enabled: false, max_attempts: 3, max_backoff: 30.0)
|
|
13
|
+
raise ArgumentError, 'max_attempts must be >= 1' if max_attempts < 1
|
|
14
|
+
raise ArgumentError, 'max_backoff must be >= 0' if max_backoff.negative?
|
|
15
|
+
|
|
16
|
+
@enabled = enabled
|
|
17
|
+
@max_attempts = max_attempts
|
|
18
|
+
@max_backoff = max_backoff.to_f
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def enabled?
|
|
22
|
+
@enabled
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
data/lib/getstream_ruby.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# Only load dotenv for .env method, not for system env method
|
|
4
4
|
require 'getstream_ruby/version'
|
|
5
|
+
require 'getstream_ruby/log_redaction'
|
|
6
|
+
require 'getstream_ruby/retry_config'
|
|
5
7
|
require 'getstream_ruby/client'
|
|
6
8
|
require 'getstream_ruby/configuration'
|
|
7
9
|
require 'getstream_ruby/errors'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: getstream-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 10.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GetStream
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dotenv
|
|
@@ -80,20 +80,6 @@ dependencies:
|
|
|
80
80
|
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '2.3'
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: faraday-retry
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - "~>"
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: '2.0'
|
|
90
|
-
type: :runtime
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - "~>"
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: '2.0'
|
|
97
83
|
- !ruby/object:Gem::Dependency
|
|
98
84
|
name: json
|
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -1534,7 +1520,10 @@ files:
|
|
|
1534
1520
|
- lib/getstream_ruby/generated/moderation_client.rb
|
|
1535
1521
|
- lib/getstream_ruby/generated/video_client.rb
|
|
1536
1522
|
- lib/getstream_ruby/generated/webhook.rb
|
|
1523
|
+
- lib/getstream_ruby/log_redaction.rb
|
|
1524
|
+
- lib/getstream_ruby/request_logging.rb
|
|
1537
1525
|
- lib/getstream_ruby/resources/feed.rb
|
|
1526
|
+
- lib/getstream_ruby/retry_config.rb
|
|
1538
1527
|
- lib/getstream_ruby/stream_response.rb
|
|
1539
1528
|
- lib/getstream_ruby/version.rb
|
|
1540
1529
|
homepage: https://getstream.io
|
|
@@ -1556,7 +1545,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
1556
1545
|
- !ruby/object:Gem::Version
|
|
1557
1546
|
version: '0'
|
|
1558
1547
|
requirements: []
|
|
1559
|
-
rubygems_version: 3.
|
|
1548
|
+
rubygems_version: 3.3.3
|
|
1560
1549
|
signing_key:
|
|
1561
1550
|
specification_version: 4
|
|
1562
1551
|
summary: Ruby SDK for GetStream
|