lago-ruby-client 1.46.0 → 1.47.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0d737a1ba08ae4f9d2a731af4cf55918555cfe91e1606fbef6126b830090e82
4
- data.tar.gz: 06b6d2a32667abe5872075cb96081aedff4d0d07fa49e0a4158f594ee66f4803
3
+ metadata.gz: f1ce7eaf525a2785545ea91a30464c1da9da45a09c95d01a6b062cd4b51a85f5
4
+ data.tar.gz: 6d8be436f3b5b4155d8e72e28aea99082fd40dfa1070a18758e40842ede13d34
5
5
  SHA512:
6
- metadata.gz: fbb748d95cb04580ecba3e5340e5587c72f4b20ce629fefc06eeb8435c074cbde11e8874407bcdf3f0f32680e961ca553b1b6ae929d695357e2bf2d21ca13041
7
- data.tar.gz: a0ef70eeb95aff16cef4492c9b2ba989323b7bae8cbc26b50a9cb3e098cd49ea54bee30fc83e6f4912f226eb5bfb538a3bb3e3f3c6829ec592b2e50bee474778
6
+ metadata.gz: c10616c41e59442007247c9245bb91be08881744dbf91f9112dbe4043e7d19c33539aa50c8367f77a06a6d3fd49efa5d5a3add3f10842eaa05cc0d0ab0f2643d
7
+ data.tar.gz: adc378ba5f462f08880bbd3ff8b841b9432660a378f3dec36aeab4b4065e0ecaf07608a27ead50a49cea6eab701c250e0f2e89915d1d13df016f22c252dc95de
@@ -53,7 +53,13 @@ module Lago
53
53
  API_PATH = 'api/v1/'
54
54
 
55
55
  class Client
56
- attr_reader :api_key, :api_url, :use_ingest_service, :ingest_api_url, :max_retries, :retry_on_rate_limit
56
+ attr_reader :api_key,
57
+ :api_url,
58
+ :use_ingest_service,
59
+ :ingest_api_url,
60
+ :max_retries,
61
+ :retry_on_rate_limit,
62
+ :on_rate_limit_info
57
63
 
58
64
  def initialize(api_key: nil, api_url: nil, use_ingest_service: false, ingest_api_url: nil, **options)
59
65
  @api_key = api_key
@@ -62,6 +68,7 @@ module Lago
62
68
  @ingest_api_url = ingest_api_url
63
69
  @max_retries = options.fetch(:max_retries, 3)
64
70
  @retry_on_rate_limit = options.fetch(:retry_on_rate_limit, true)
71
+ @on_rate_limit_info = options[:on_rate_limit_info]
65
72
  end
66
73
 
67
74
  def base_api_url
@@ -11,84 +11,67 @@ module Lago
11
11
  BACKOFF_MULTIPLIER = 2
12
12
  MAX_RETRY_DELAY = 20
13
13
 
14
- def initialize(api_key, uri, max_retries: DEFAULT_MAX_RETRIES, retry_on_rate_limit: true)
14
+ def initialize(
15
+ api_key,
16
+ uri,
17
+ max_retries: DEFAULT_MAX_RETRIES,
18
+ retry_on_rate_limit: true,
19
+ on_rate_limit_info: nil
20
+ )
15
21
  @api_key = api_key
16
22
  @uri = uri
17
23
  @max_retries = max_retries
18
24
  @retry_on_rate_limit = retry_on_rate_limit
25
+ @on_rate_limit_info = on_rate_limit_info
19
26
  end
20
27
 
21
28
  def post(body, path = uri.path)
22
- execute_request do
23
- http_client.send_request(
24
- 'POST',
25
- path,
26
- prepare_payload(body),
27
- headers
28
- )
29
+ method = 'POST'
30
+ execute_request(method:) do
31
+ http_client.send_request(method, path, prepare_payload(body), headers)
29
32
  end
30
33
  end
31
34
 
32
35
  def put(path = uri.path, identifier:, body:)
36
+ method = 'PUT'
33
37
  uri_path = identifier.nil? ? path : "#{path}/#{CGI.escapeURIComponent(identifier)}"
34
- execute_request do
35
- http_client.send_request(
36
- 'PUT',
37
- uri_path,
38
- prepare_payload(body),
39
- headers
40
- )
38
+ execute_request(method:) do
39
+ http_client.send_request(method, uri_path, prepare_payload(body), headers)
41
40
  end
42
41
  end
43
42
 
44
43
  def patch(path = uri.path, identifier:, body:)
44
+ method = 'PATCH'
45
45
  uri_path = identifier.nil? ? path : "#{path}/#{CGI.escapeURIComponent(identifier)}"
46
- execute_request do
47
- http_client.send_request(
48
- 'PATCH',
49
- uri_path,
50
- prepare_payload(body),
51
- headers
52
- )
46
+ execute_request(method:) do
47
+ http_client.send_request(method, uri_path, prepare_payload(body), headers)
53
48
  end
54
49
  end
55
50
 
56
51
  def get(path = uri.path, identifier:)
52
+ method = 'GET'
57
53
  uri_path = identifier.nil? ? path : "#{path}/#{CGI.escapeURIComponent(identifier)}"
58
- execute_request do
59
- http_client.send_request(
60
- 'GET',
61
- uri_path,
62
- prepare_payload(nil),
63
- headers
64
- )
54
+ execute_request(method:) do
55
+ http_client.send_request(method, uri_path, prepare_payload(nil), headers)
65
56
  end
66
57
  end
67
58
 
68
59
  def destroy(path = uri.path, identifier:, options: nil)
60
+ method = 'DELETE'
69
61
  uri_path = path
70
62
  uri_path += "/#{CGI.escapeURIComponent(identifier)}" if identifier
71
63
  uri_path += "?#{URI.encode_www_form(options)}" unless options.nil?
72
- execute_request do
73
- http_client.send_request(
74
- 'DELETE',
75
- uri_path,
76
- prepare_payload(nil),
77
- headers
78
- )
64
+ execute_request(method:) do
65
+ http_client.send_request(method, uri_path, prepare_payload(nil), headers)
79
66
  end
80
67
  end
81
68
 
82
69
  def get_all(options, path = uri.path)
70
+ method = 'GET'
83
71
  uri_path = options.empty? ? path : "#{path}?#{URI.encode_www_form(options)}"
84
72
 
85
- execute_request do
86
- http_client.send_request(
87
- 'GET',
88
- uri_path,
89
- prepare_payload(nil),
90
- headers
91
- )
73
+ execute_request(method:) do
74
+ http_client.send_request(method, uri_path, prepare_payload(nil), headers)
92
75
  end
93
76
  end
94
77
 
@@ -104,29 +87,41 @@ module Lago
104
87
  }
105
88
  end
106
89
 
107
- def execute_request(retry_count = 0, &block)
90
+ def execute_request(retry_count = 0, method: nil, &block)
108
91
  response = yield
109
- handle_response(response, retry_count, block)
92
+ handle_response(response, retry_count, block, method:)
110
93
  end
111
94
 
112
- def handle_response(response, retry_count, block)
95
+ def handle_response(response, retry_count, block, method: nil)
113
96
  code = response.code.to_i
114
97
 
115
98
  if code == 429 && @retry_on_rate_limit && retry_count < @max_retries
116
- handle_rate_limit(response, retry_count, block)
99
+ handle_rate_limit(response, retry_count, block, method:)
117
100
  elsif !RESPONSE_SUCCESS_CODES.include?(code)
118
101
  raise_error(response)
119
102
  else
103
+ emit_rate_limit_info(response, method:)
120
104
  parse_response_body(response)
121
105
  end
122
106
  rescue JSON::ParserError
123
107
  response.body
124
108
  end
125
109
 
126
- def handle_rate_limit(response, retry_count, block)
110
+ def handle_rate_limit(response, retry_count, block, method: nil)
127
111
  reset_seconds = extract_reset_seconds(response, retry_count)
128
112
  sleep(reset_seconds)
129
- execute_request(retry_count + 1, &block)
113
+ execute_request(retry_count + 1, method:, &block)
114
+ end
115
+
116
+ def emit_rate_limit_info(response, method: nil)
117
+ return if @on_rate_limit_info.nil?
118
+
119
+ info = Lago::Api::RateLimitInfo.parse(response, method:, url: uri.to_s)
120
+ return if info.nil?
121
+
122
+ @on_rate_limit_info.call(info)
123
+ rescue StandardError => e
124
+ warn("Lago: on_rate_limit_info callback raised: #{e.class}: #{e.message}")
130
125
  end
131
126
 
132
127
  def parse_response_body(response)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Lago
6
+ module Api
7
+ # Ready-to-use +on_rate_limit_info+ callable that logs a warning each time
8
+ # rate limit usage crosses one of the configured thresholds.
9
+ #
10
+ # Example:
11
+ # client = Lago::Api::Client.new(
12
+ # api_key: '...',
13
+ # on_rate_limit_info: Lago::Api::LoggingRateLimitObserver.new,
14
+ # )
15
+ class LoggingRateLimitObserver
16
+ DEFAULT_THRESHOLDS = [0.80, 0.90, 0.95].freeze
17
+
18
+ def initialize(thresholds: DEFAULT_THRESHOLDS, logger: nil, level: Logger::WARN)
19
+ @thresholds = thresholds.sort.reverse
20
+ @logger = logger || default_logger
21
+ @level = level
22
+ end
23
+
24
+ def call(info)
25
+ pct = info.usage_pct
26
+ return if pct.nil?
27
+
28
+ return unless @thresholds.any? { |threshold| pct >= threshold }
29
+
30
+ @logger.add(
31
+ @level,
32
+ format(
33
+ 'Lago rate limit at %<pct>.0f%% (limit=%<limit>s, remaining=%<remaining>s, ' \
34
+ 'reset=%<reset>ss, %<method>s %<url>s)',
35
+ pct: pct * 100,
36
+ limit: info.limit.inspect,
37
+ remaining: info.remaining.inspect,
38
+ reset: info.reset.inspect,
39
+ method: info.method,
40
+ url: info.url,
41
+ ),
42
+ )
43
+ end
44
+
45
+ private
46
+
47
+ def default_logger
48
+ logger = Logger.new($stderr)
49
+ logger.progname = 'lago_ruby_client.rate_limit'
50
+ logger
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lago
4
+ module Api
5
+ # Parsed rate limit headers from a Lago API response.
6
+ #
7
+ # Delivered to the +on_rate_limit_info+ callback after every successful
8
+ # request so callers can build observability around the rate limit
9
+ # (warn at thresholds, emit metrics, etc.).
10
+ class RateLimitInfo
11
+ attr_reader :limit, :remaining, :reset, :method, :url
12
+
13
+ # Parses x-ratelimit-* headers from a Net::HTTPResponse-like object.
14
+ # Returns +nil+ when no rate limit headers are present.
15
+ def self.parse(response, method:, url:)
16
+ limit = response['x-ratelimit-limit']
17
+ remaining = response['x-ratelimit-remaining']
18
+ reset = response['x-ratelimit-reset']
19
+
20
+ return nil if limit.nil? && remaining.nil? && reset.nil?
21
+
22
+ new(
23
+ limit: limit&.to_i,
24
+ remaining: remaining&.to_i,
25
+ reset: reset&.to_i,
26
+ method:,
27
+ url:,
28
+ )
29
+ end
30
+
31
+ def initialize(limit:, remaining:, reset:, method:, url:)
32
+ @limit = limit
33
+ @remaining = remaining
34
+ @reset = reset
35
+ @method = method
36
+ @url = url
37
+ end
38
+
39
+ # Returns the fraction of the rate limit currently used as a Float in
40
+ # [0.0, 1.0], or +nil+ when the headers aren't usable (missing limit,
41
+ # zero limit, missing remaining).
42
+ def usage_pct
43
+ return nil if limit.nil? || remaining.nil? || limit.to_i <= 0
44
+
45
+ 1.0 - (remaining.to_f / limit)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -64,6 +64,7 @@ module Lago
64
64
  uri,
65
65
  max_retries: client.max_retries,
66
66
  retry_on_rate_limit: client.retry_on_rate_limit,
67
+ on_rate_limit_info: client.on_rate_limit_info,
67
68
  )
68
69
  end
69
70
  end
@@ -20,10 +20,15 @@ module Lago
20
20
 
21
21
  def current_usage( # rubocop:disable Metrics/ParameterLists
22
22
  external_customer_id, external_subscription_id, apply_taxes: nil,
23
+ charge_id: nil, charge_code: nil, billable_metric_code: nil, group: nil,
23
24
  filter_by_charge_id: nil, filter_by_charge_code: nil, filter_by_group: nil, full_usage: nil
24
25
  )
25
26
  query_params = { external_subscription_id: external_subscription_id }
26
27
  query_params[:apply_taxes] = apply_taxes unless apply_taxes.nil?
28
+ query_params[:charge_id] = charge_id unless charge_id.nil?
29
+ query_params[:charge_code] = charge_code unless charge_code.nil?
30
+ query_params[:billable_metric_code] = billable_metric_code unless billable_metric_code.nil?
31
+ group&.each { |k, v| query_params[:"group[#{k}]"] = v }
27
32
  query_params[:filter_by_charge_id] = filter_by_charge_id unless filter_by_charge_id.nil?
28
33
  query_params[:filter_by_charge_code] = filter_by_charge_code unless filter_by_charge_code.nil?
29
34
  filter_by_group&.each { |k, v| query_params[:"filter_by_group[#{k}]"] = v }
@@ -21,6 +21,7 @@ module Lago
21
21
  uri,
22
22
  max_retries: client.max_retries,
23
23
  retry_on_rate_limit: client.retry_on_rate_limit,
24
+ on_rate_limit_info: client.on_rate_limit_info,
24
25
  )
25
26
 
26
27
  payload = whitelist_params(params)
@@ -13,6 +13,7 @@ module Lago
13
13
  client.base_api_url,
14
14
  max_retries: client.max_retries,
15
15
  retry_on_rate_limit: client.retry_on_rate_limit,
16
+ on_rate_limit_info: client.on_rate_limit_info,
16
17
  )
17
18
  end
18
19
 
data/lib/lago/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lago
4
- VERSION = '1.46.0'
4
+ VERSION = '1.47.0'
5
5
  end
@@ -12,3 +12,5 @@ require 'lago/api/client'
12
12
  require 'lago/api/connection'
13
13
  require 'lago/api/http_error'
14
14
  require 'lago/api/rate_limit_error'
15
+ require 'lago/api/rate_limit_info'
16
+ require 'lago/api/logging_rate_limit_observer'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lago-ruby-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.46.0
4
+ version: 1.47.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lovro Colic
@@ -313,7 +313,9 @@ files:
313
313
  - lib/lago/api/client.rb
314
314
  - lib/lago/api/connection.rb
315
315
  - lib/lago/api/http_error.rb
316
+ - lib/lago/api/logging_rate_limit_observer.rb
316
317
  - lib/lago/api/rate_limit_error.rb
318
+ - lib/lago/api/rate_limit_info.rb
317
319
  - lib/lago/api/resources/activity_log.rb
318
320
  - lib/lago/api/resources/add_on.rb
319
321
  - lib/lago/api/resources/api_log.rb