apisonator 2.100.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (173) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +317 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.base +65 -0
  5. data/Gemfile.lock +319 -0
  6. data/Gemfile.on_prem +1 -0
  7. data/Gemfile.on_prem.lock +297 -0
  8. data/LICENSE +202 -0
  9. data/NOTICE +15 -0
  10. data/README.md +230 -0
  11. data/Rakefile +287 -0
  12. data/apisonator.gemspec +47 -0
  13. data/app/api/api.rb +13 -0
  14. data/app/api/internal/alert_limits.rb +32 -0
  15. data/app/api/internal/application_keys.rb +49 -0
  16. data/app/api/internal/application_referrer_filters.rb +43 -0
  17. data/app/api/internal/applications.rb +77 -0
  18. data/app/api/internal/errors.rb +54 -0
  19. data/app/api/internal/events.rb +42 -0
  20. data/app/api/internal/internal.rb +104 -0
  21. data/app/api/internal/metrics.rb +40 -0
  22. data/app/api/internal/service_tokens.rb +46 -0
  23. data/app/api/internal/services.rb +58 -0
  24. data/app/api/internal/stats.rb +42 -0
  25. data/app/api/internal/usagelimits.rb +62 -0
  26. data/app/api/internal/utilization.rb +23 -0
  27. data/bin/3scale_backend +223 -0
  28. data/bin/3scale_backend_worker +26 -0
  29. data/config.ru +4 -0
  30. data/config/puma.rb +192 -0
  31. data/config/schedule.rb +9 -0
  32. data/ext/mkrf_conf.rb +64 -0
  33. data/lib/3scale/backend.rb +67 -0
  34. data/lib/3scale/backend/alert_limit.rb +56 -0
  35. data/lib/3scale/backend/alerts.rb +137 -0
  36. data/lib/3scale/backend/analytics/kinesis.rb +3 -0
  37. data/lib/3scale/backend/analytics/kinesis/adapter.rb +180 -0
  38. data/lib/3scale/backend/analytics/kinesis/exporter.rb +86 -0
  39. data/lib/3scale/backend/analytics/kinesis/job.rb +135 -0
  40. data/lib/3scale/backend/analytics/redshift.rb +3 -0
  41. data/lib/3scale/backend/analytics/redshift/adapter.rb +367 -0
  42. data/lib/3scale/backend/analytics/redshift/importer.rb +83 -0
  43. data/lib/3scale/backend/analytics/redshift/job.rb +33 -0
  44. data/lib/3scale/backend/application.rb +330 -0
  45. data/lib/3scale/backend/application_events.rb +76 -0
  46. data/lib/3scale/backend/background_job.rb +65 -0
  47. data/lib/3scale/backend/configurable.rb +20 -0
  48. data/lib/3scale/backend/configuration.rb +151 -0
  49. data/lib/3scale/backend/configuration/loader.rb +42 -0
  50. data/lib/3scale/backend/constants.rb +19 -0
  51. data/lib/3scale/backend/cors.rb +84 -0
  52. data/lib/3scale/backend/distributed_lock.rb +67 -0
  53. data/lib/3scale/backend/environment.rb +21 -0
  54. data/lib/3scale/backend/error_storage.rb +52 -0
  55. data/lib/3scale/backend/errors.rb +343 -0
  56. data/lib/3scale/backend/event_storage.rb +120 -0
  57. data/lib/3scale/backend/experiment.rb +84 -0
  58. data/lib/3scale/backend/extensions.rb +5 -0
  59. data/lib/3scale/backend/extensions/array.rb +19 -0
  60. data/lib/3scale/backend/extensions/hash.rb +26 -0
  61. data/lib/3scale/backend/extensions/nil_class.rb +13 -0
  62. data/lib/3scale/backend/extensions/redis.rb +44 -0
  63. data/lib/3scale/backend/extensions/string.rb +13 -0
  64. data/lib/3scale/backend/extensions/time.rb +110 -0
  65. data/lib/3scale/backend/failed_jobs_scheduler.rb +141 -0
  66. data/lib/3scale/backend/job_fetcher.rb +122 -0
  67. data/lib/3scale/backend/listener.rb +728 -0
  68. data/lib/3scale/backend/listener_metrics.rb +99 -0
  69. data/lib/3scale/backend/logging.rb +48 -0
  70. data/lib/3scale/backend/logging/external.rb +44 -0
  71. data/lib/3scale/backend/logging/external/impl.rb +93 -0
  72. data/lib/3scale/backend/logging/external/impl/airbrake.rb +66 -0
  73. data/lib/3scale/backend/logging/external/impl/bugsnag.rb +69 -0
  74. data/lib/3scale/backend/logging/external/impl/default.rb +18 -0
  75. data/lib/3scale/backend/logging/external/resque.rb +57 -0
  76. data/lib/3scale/backend/logging/logger.rb +18 -0
  77. data/lib/3scale/backend/logging/middleware.rb +62 -0
  78. data/lib/3scale/backend/logging/middleware/json_writer.rb +21 -0
  79. data/lib/3scale/backend/logging/middleware/text_writer.rb +60 -0
  80. data/lib/3scale/backend/logging/middleware/writer.rb +143 -0
  81. data/lib/3scale/backend/logging/worker.rb +107 -0
  82. data/lib/3scale/backend/manifest.rb +80 -0
  83. data/lib/3scale/backend/memoizer.rb +277 -0
  84. data/lib/3scale/backend/metric.rb +275 -0
  85. data/lib/3scale/backend/metric/collection.rb +91 -0
  86. data/lib/3scale/backend/oauth.rb +4 -0
  87. data/lib/3scale/backend/oauth/token.rb +26 -0
  88. data/lib/3scale/backend/oauth/token_key.rb +30 -0
  89. data/lib/3scale/backend/oauth/token_storage.rb +313 -0
  90. data/lib/3scale/backend/oauth/token_value.rb +25 -0
  91. data/lib/3scale/backend/period.rb +3 -0
  92. data/lib/3scale/backend/period/boundary.rb +107 -0
  93. data/lib/3scale/backend/period/cache.rb +28 -0
  94. data/lib/3scale/backend/period/period.rb +402 -0
  95. data/lib/3scale/backend/queue_storage.rb +16 -0
  96. data/lib/3scale/backend/rack.rb +49 -0
  97. data/lib/3scale/backend/rack/exception_catcher.rb +136 -0
  98. data/lib/3scale/backend/rack/internal_error_catcher.rb +23 -0
  99. data/lib/3scale/backend/rack/prometheus.rb +19 -0
  100. data/lib/3scale/backend/saas.rb +6 -0
  101. data/lib/3scale/backend/saas_analytics.rb +4 -0
  102. data/lib/3scale/backend/server.rb +30 -0
  103. data/lib/3scale/backend/server/falcon.rb +52 -0
  104. data/lib/3scale/backend/server/puma.rb +71 -0
  105. data/lib/3scale/backend/service.rb +317 -0
  106. data/lib/3scale/backend/service_token.rb +97 -0
  107. data/lib/3scale/backend/stats.rb +8 -0
  108. data/lib/3scale/backend/stats/aggregator.rb +170 -0
  109. data/lib/3scale/backend/stats/aggregators/base.rb +72 -0
  110. data/lib/3scale/backend/stats/aggregators/response_code.rb +58 -0
  111. data/lib/3scale/backend/stats/aggregators/usage.rb +34 -0
  112. data/lib/3scale/backend/stats/bucket_reader.rb +135 -0
  113. data/lib/3scale/backend/stats/bucket_storage.rb +108 -0
  114. data/lib/3scale/backend/stats/cleaner.rb +195 -0
  115. data/lib/3scale/backend/stats/codes_commons.rb +14 -0
  116. data/lib/3scale/backend/stats/delete_job_def.rb +60 -0
  117. data/lib/3scale/backend/stats/key_generator.rb +73 -0
  118. data/lib/3scale/backend/stats/keys.rb +104 -0
  119. data/lib/3scale/backend/stats/partition_eraser_job.rb +58 -0
  120. data/lib/3scale/backend/stats/partition_generator_job.rb +46 -0
  121. data/lib/3scale/backend/stats/period_commons.rb +34 -0
  122. data/lib/3scale/backend/stats/stats_parser.rb +141 -0
  123. data/lib/3scale/backend/stats/storage.rb +113 -0
  124. data/lib/3scale/backend/statsd.rb +14 -0
  125. data/lib/3scale/backend/storable.rb +35 -0
  126. data/lib/3scale/backend/storage.rb +40 -0
  127. data/lib/3scale/backend/storage_async.rb +4 -0
  128. data/lib/3scale/backend/storage_async/async_redis.rb +21 -0
  129. data/lib/3scale/backend/storage_async/client.rb +205 -0
  130. data/lib/3scale/backend/storage_async/pipeline.rb +79 -0
  131. data/lib/3scale/backend/storage_async/resque_extensions.rb +30 -0
  132. data/lib/3scale/backend/storage_helpers.rb +278 -0
  133. data/lib/3scale/backend/storage_key_helpers.rb +9 -0
  134. data/lib/3scale/backend/storage_sync.rb +43 -0
  135. data/lib/3scale/backend/transaction.rb +62 -0
  136. data/lib/3scale/backend/transactor.rb +177 -0
  137. data/lib/3scale/backend/transactor/limit_headers.rb +54 -0
  138. data/lib/3scale/backend/transactor/notify_batcher.rb +139 -0
  139. data/lib/3scale/backend/transactor/notify_job.rb +47 -0
  140. data/lib/3scale/backend/transactor/process_job.rb +33 -0
  141. data/lib/3scale/backend/transactor/report_job.rb +84 -0
  142. data/lib/3scale/backend/transactor/status.rb +236 -0
  143. data/lib/3scale/backend/transactor/usage_report.rb +182 -0
  144. data/lib/3scale/backend/usage.rb +63 -0
  145. data/lib/3scale/backend/usage_limit.rb +115 -0
  146. data/lib/3scale/backend/use_cases/provider_key_change_use_case.rb +60 -0
  147. data/lib/3scale/backend/util.rb +17 -0
  148. data/lib/3scale/backend/validators.rb +26 -0
  149. data/lib/3scale/backend/validators/base.rb +36 -0
  150. data/lib/3scale/backend/validators/key.rb +17 -0
  151. data/lib/3scale/backend/validators/limits.rb +57 -0
  152. data/lib/3scale/backend/validators/oauth_key.rb +15 -0
  153. data/lib/3scale/backend/validators/oauth_setting.rb +15 -0
  154. data/lib/3scale/backend/validators/redirect_uri.rb +33 -0
  155. data/lib/3scale/backend/validators/referrer.rb +60 -0
  156. data/lib/3scale/backend/validators/service_state.rb +15 -0
  157. data/lib/3scale/backend/validators/state.rb +15 -0
  158. data/lib/3scale/backend/version.rb +5 -0
  159. data/lib/3scale/backend/views/oauth_access_tokens.builder +14 -0
  160. data/lib/3scale/backend/views/oauth_app_id_by_token.builder +4 -0
  161. data/lib/3scale/backend/worker.rb +87 -0
  162. data/lib/3scale/backend/worker_async.rb +88 -0
  163. data/lib/3scale/backend/worker_metrics.rb +44 -0
  164. data/lib/3scale/backend/worker_sync.rb +32 -0
  165. data/lib/3scale/bundler_shim.rb +17 -0
  166. data/lib/3scale/prometheus_server.rb +10 -0
  167. data/lib/3scale/tasks/connectivity.rake +41 -0
  168. data/lib/3scale/tasks/helpers.rb +3 -0
  169. data/lib/3scale/tasks/helpers/environment.rb +23 -0
  170. data/lib/3scale/tasks/stats.rake +131 -0
  171. data/lib/3scale/tasks/swagger.rake +46 -0
  172. data/licenses.xml +1215 -0
  173. metadata +227 -0
@@ -0,0 +1,47 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module Transactor
4
+ #
5
+ # Job for notifying about backend calls.
6
+ class NotifyJob < BackgroundJob
7
+ extend Configurable
8
+ @queue = :main
9
+
10
+ InvalidMasterServiceId = Class.new(ThreeScale::Backend::Error)
11
+
12
+ class << self
13
+ def perform_logged(provider_key, usage, timestamp, _enqueue_time)
14
+ application_id = Application.load_id_by_key(master_service_id, provider_key)
15
+
16
+ if application_id && Application.exists?(master_service_id, application_id)
17
+ master_metrics = Metric.load_all(master_service_id)
18
+
19
+ ProcessJob.perform([{
20
+ service_id: master_service_id,
21
+ application_id: application_id,
22
+ timestamp: timestamp,
23
+ usage: master_metrics.process_usage(usage)
24
+ }])
25
+ end
26
+ [true, "#{provider_key} #{application_id || '--'}"]
27
+ end
28
+
29
+ private
30
+
31
+ def master_service_id
32
+ value = configuration.master_service_id
33
+
34
+ unless value
35
+ raise InvalidMasterServiceId,
36
+ "Can't find master service id. Make sure the \"master_service_id\" "\
37
+ 'configuration value is set correctly'
38
+ end
39
+
40
+ value.to_s
41
+ end
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ require '3scale/backend/transaction'
2
+
3
+ module ThreeScale
4
+ module Backend
5
+ module Transactor
6
+ # Job for processing (aggregating and archiving) transactions.
7
+
8
+ ## WARNING: This is not a resque job, the .perform is called by another
9
+ ## job, either Report or NotifyJob it's meant to be like this in case we
10
+ ## want to detach it further
11
+ class ProcessJob
12
+
13
+ class << self
14
+ def perform(transactions)
15
+ transactions = preprocess(transactions)
16
+ Stats::Aggregator.process(transactions)
17
+ end
18
+
19
+ private
20
+
21
+ def preprocess(transactions)
22
+ transactions.map do |transaction_attrs|
23
+ transaction = Transaction.new(transaction_attrs)
24
+ transaction.ensure_on_time!
25
+ transaction
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,84 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module Transactor
4
+
5
+ # Job for reporting transactions.
6
+ class ReportJob < BackgroundJob
7
+ @queue = :priority
8
+
9
+ class << self
10
+ def perform_logged(service_id, raw_transactions, _enqueue_time, context_info = {})
11
+ context_info ||= {} # avoid nils potentially existing in older versions
12
+ request_info = context_info['request'.freeze] || {}
13
+
14
+ transactions = parse_transactions(service_id, raw_transactions, request_info)
15
+ ProcessJob.perform(transactions) if !transactions.nil? && transactions.size > 0
16
+
17
+ # Last field was logs.size. Set it to 0 until we modify our parsers.
18
+ [true, "#{service_id} #{transactions.size} 0"]
19
+ rescue Error => error
20
+ ErrorStorage.store(service_id, error, context_info)
21
+ [false, "#{service_id} #{error}"]
22
+ rescue Exception => error
23
+ if error.class == ArgumentError &&
24
+ error.message == "invalid byte sequence in UTF-8"
25
+
26
+ ErrorStorage.store(service_id, NotValidData.new, context_info)
27
+ [false, "#{service_id} #{error}"]
28
+ else
29
+ raise error
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def enqueue_time(args)
36
+ args[2]
37
+ end
38
+
39
+ def parse_transactions(service_id, raw_transactions, request_info)
40
+ transactions = []
41
+ exts = request_info['extensions'.freeze]&.symbolize_names || {}
42
+
43
+ group_by_application_id(service_id, raw_transactions) do |app_id, group|
44
+ group.each do |raw_transaction|
45
+
46
+ transaction = compose_transaction(service_id, app_id, raw_transaction, exts)
47
+ log = raw_transaction['log']
48
+
49
+ transaction[:response_code] = log['code'] if transaction && log
50
+
51
+ transactions << transaction if transaction
52
+ end
53
+ end
54
+
55
+ transactions
56
+ end
57
+
58
+ def group_by_application_id(service_id, transactions, &block)
59
+ return [] if transactions.empty?
60
+ transactions = transactions.values if transactions.respond_to?(:values)
61
+ transactions.group_by do |transaction|
62
+ Application.extract_id!(service_id, transaction['app_id'], transaction['user_key'], transaction['access_token'])
63
+ end.each(&block)
64
+ end
65
+
66
+ def compose_transaction(service_id, app_id, raw_transaction, extensions)
67
+ usage = raw_transaction['usage']
68
+
69
+ if usage && !usage.empty?
70
+ metrics = Metric.load_all(service_id)
71
+ {
72
+ service_id: service_id,
73
+ application_id: app_id,
74
+ timestamp: raw_transaction['timestamp'],
75
+ usage: metrics.process_usage(usage,
76
+ extensions[:flat_usage] == '1'),
77
+ }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,236 @@
1
+ require 'json'
2
+
3
+ module ThreeScale
4
+ module Backend
5
+ module Transactor
6
+ class Status
7
+ # This is the default field we respond with when using OAuth redirects
8
+ # We only use 'redirect_uri' if a request sent such a param. See #397.
9
+ REDIRECT_URI_FIELD = 'redirect_url'.freeze
10
+ private_constant :REDIRECT_URI_FIELD
11
+
12
+ def initialize(attributes)
13
+ @service_id = attributes[:service_id]
14
+ @application = attributes[:application]
15
+ @oauth = attributes[:oauth]
16
+ @usage = attributes[:usage]
17
+ @predicted_usage = attributes[:predicted_usage]
18
+ @values = filter_values(attributes[:values] || {})
19
+ @timestamp = attributes[:timestamp] || Time.now.getutc
20
+ @hierarchy_ext = attributes[:hierarchy]
21
+ @flat_usage_ext = attributes[:flat_usage]
22
+
23
+ raise 'service_id not specified' if @service_id.nil?
24
+ raise ':application is required' if @application.nil?
25
+
26
+ @redirect_uri_field = REDIRECT_URI_FIELD
27
+ @authorized = true
28
+ end
29
+
30
+ attr_reader :service_id
31
+ attr_reader :application
32
+ attr_reader :oauth
33
+ attr_accessor :redirect_uri_field
34
+ attr_accessor :values
35
+
36
+ def flat_usage
37
+ @flat_usage_ext
38
+ end
39
+
40
+ def reject!(error)
41
+ @authorized = false
42
+ @rejection_reason_code ||= error.code
43
+ @rejection_reason_text ||= error.message
44
+ end
45
+
46
+ attr_reader :timestamp
47
+ attr_reader :rejection_reason_code
48
+ attr_reader :rejection_reason_text
49
+
50
+ # Returns the usage to be reported in an authrep request.
51
+ def usage
52
+ @predicted_usage ? nil : @usage
53
+ end
54
+
55
+ # Returns the predicted usage of an authorize request.
56
+ def predicted_usage
57
+ @predicted_usage ? @usage : nil
58
+ end
59
+
60
+ # Returns the actual usage. If there isn't one, returns the predicted
61
+ # usage. If there isn't an actual or predicted usage, returns nil.
62
+ def actual_or_predicted_usage
63
+ usage || predicted_usage
64
+ end
65
+
66
+ def authorized?
67
+ @authorized
68
+ end
69
+
70
+ def plan_name
71
+ @application.plan_name unless @application.nil?
72
+ end
73
+
74
+ def application_usage_reports
75
+ @usage_report ||= load_usage_reports @application
76
+ end
77
+
78
+ def value_for_usage_limit(usage_limit)
79
+ values = @values[usage_limit.period]
80
+ values && values[usage_limit.metric_id] || 0
81
+ end
82
+
83
+ def value_for_application_usage_limit(usage_limit)
84
+ values = @values[usage_limit.period]
85
+ values && values[usage_limit.metric_id] || 0
86
+ end
87
+
88
+ # provides a hierarchy hash with metrics as symbolic names
89
+ def hierarchy
90
+ @hierarchy ||= Metric.hierarchy service_id
91
+ end
92
+
93
+ def limit_headers(now = Time.now.utc)
94
+ # maybe filter by exceeded reports if not authorized
95
+ LimitHeaders.get(reports_to_calculate_limit_headers, now)
96
+ end
97
+
98
+ def to_xml(options = {})
99
+ xml = ''
100
+ xml << '<?xml version="1.0" encoding="UTF-8"?>'.freeze unless options[:skip_instruct]
101
+ xml << '<status>'.freeze
102
+
103
+ add_authorize_section(xml)
104
+
105
+ if oauth
106
+ add_application_section(xml)
107
+ end
108
+
109
+ hierarchy_reports = [] if @hierarchy_ext
110
+ if !@application.nil?
111
+ add_plan_section(xml, 'plan'.freeze, plan_name)
112
+ add_reports_section(xml, application_usage_reports)
113
+ hierarchy_reports.concat application_usage_reports if hierarchy_reports
114
+ end
115
+
116
+ if hierarchy_reports
117
+ add_hierarchy_section(xml, hierarchy_reports)
118
+ end
119
+
120
+ xml << '</status>'.freeze
121
+ end
122
+
123
+ private
124
+
125
+ # Returns the app usage reports needed to construct the limit headers.
126
+ # If the status does not have a 'usage', this method returns all the
127
+ # usage reports. Otherwise, it returns the reports associated with the
128
+ # metrics present in the 'usage' and their parents.
129
+ def reports_to_calculate_limit_headers
130
+ all_reports = application_usage_reports
131
+
132
+ return all_reports if (@usage.nil? || @usage.empty?)
133
+
134
+ metric_names_in_usage = @usage.keys
135
+ metrics = metric_names_in_usage | ascendants_names(metric_names_in_usage)
136
+ all_reports.select { |report| metrics.include?(report.metric_name) }
137
+ end
138
+
139
+ def ascendants_names(metric_names)
140
+ metric_names.flat_map do |metric_name|
141
+ Metric.ascendants(@service_id, metric_name)
142
+ end
143
+ end
144
+
145
+ # make sure the keys are Periods
146
+ def filter_values(values)
147
+ return nil if values.nil?
148
+ values.inject({}) do |acc, (k, v)|
149
+ key = begin
150
+ Period[k]
151
+ rescue Period::Unknown
152
+ k
153
+ end
154
+ acc[key] = v
155
+ acc
156
+ end
157
+ end
158
+
159
+ def add_hierarchy_section(xml, reports)
160
+ xml << '<hierarchy>'.freeze
161
+ with_report_and_hierarchy(reports) do |ur, children|
162
+ xml << '<metric name="'.freeze
163
+ xml << ur.metric_name << '" children="'.freeze
164
+ xml << (children ? children.join(' '.freeze) : '') << '"/>'.freeze
165
+ end
166
+ xml << '</hierarchy>'.freeze
167
+ end
168
+
169
+ # helper to iterate over reports and get relevant hierarchy info
170
+ def with_report_and_hierarchy(reports)
171
+ reports.each do |ur|
172
+ yield ur, hierarchy[ur.metric_name]
173
+ end
174
+ end
175
+
176
+ def add_plan_section(xml, tag, plan_name)
177
+ xml << "<#{tag}>"
178
+ xml << plan_name.to_s.encode(xml: :text) << "</#{tag}>"
179
+ end
180
+
181
+ def add_authorize_section(xml)
182
+ if authorized?
183
+ xml << '<authorized>true</authorized>'.freeze
184
+ else
185
+ xml << '<authorized>false</authorized><reason>'.freeze
186
+ xml << rejection_reason_text
187
+ xml << '</reason>'.freeze
188
+ end
189
+ end
190
+
191
+ def add_application_section(xml)
192
+ redirect_uri = application.redirect_url
193
+ xml << '<application>' \
194
+ "<id>#{application.id}</id>" \
195
+ "<key>#{application.keys.first}</key>" \
196
+ "<#{@redirect_uri_field}>#{redirect_uri}</#{@redirect_uri_field}>" \
197
+ '</application>'
198
+ end
199
+
200
+ def load_usage_reports(application)
201
+ # We might have usage limits that apply to metrics that no longer
202
+ # exist. In that case, the usage limit refers to a metric ID that no
203
+ # longer has a name associated to it. When that happens, we do not
204
+ # want to take into account that usage limit.
205
+ # This might happen, for example, when a Backend client decides to delete
206
+ # a metric and all the associated usage limits, but the operation fails
207
+ # for some of the usage limits.
208
+
209
+ reports = []
210
+ if !application.nil?
211
+ application.usage_limits.each do |usage_limit|
212
+ if application.metric_name(usage_limit.metric_id)
213
+ reports << UsageReport.new(self, usage_limit)
214
+ end
215
+ end
216
+ end
217
+ reports
218
+ end
219
+
220
+ def add_reports_section(xml, reports)
221
+ unless reports.empty?
222
+ xml_node = 'usage_reports>'.freeze
223
+ xml << '<'.freeze
224
+ xml << xml_node
225
+ reports.each do |report|
226
+ xml << report.to_xml
227
+ end
228
+ xml << '</'.freeze
229
+ xml << xml_node
230
+ end
231
+ end
232
+
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,182 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module Transactor
4
+ class Status
5
+ class UsageReport
6
+ attr_reader :type, :period
7
+
8
+ def initialize(status, usage_limit)
9
+ @status = status
10
+ @usage_limit = usage_limit
11
+ @period = usage_limit.period.new(status.timestamp)
12
+ end
13
+
14
+ def metric_name
15
+ @metric_name ||= @status.application.metric_name(metric_id)
16
+ end
17
+
18
+ def metric_id
19
+ @usage_limit.metric_id
20
+ end
21
+
22
+ def max_value
23
+ @usage_limit.value
24
+ end
25
+
26
+ def current_value
27
+ @current_value ||= @status.value_for_usage_limit(@usage_limit)
28
+ end
29
+
30
+ # Returns -1 if the period is eternity. Otherwise, returns the time
31
+ # remaining until the end of the period in seconds.
32
+ def remaining_time(from = Time.now)
33
+ if period.granularity == Period::Granularity::Eternity
34
+ -1
35
+ else
36
+ (period.finish - from).ceil
37
+ end
38
+ end
39
+
40
+ # Returns the number of identical calls that can be made before
41
+ # violating the limits defined in the usage report.
42
+ #
43
+ # Authrep (with actual usage): suppose that we have a metric with a
44
+ # daily limit of 10, a current usage of 0, and a given usage of 2.
45
+ # After taking into account the given usage, the number of identical
46
+ # calls that could be performed is (10-2)/2 = 4.
47
+ #
48
+ # Authorize (with predicted usage): suppose that we have a metric
49
+ # with a daily limit of 10, a current usage of 0, and a given usage
50
+ # of 2. This time, the given usage is not taken into account, as it
51
+ # is predicted, not to be reported. The number of identical calls
52
+ # that could be performed is 10/2 = 5.
53
+ #
54
+ # Returns -1 when there is not a limit in the number of calls.
55
+ def remaining_same_calls
56
+ return 0 if remaining <= 0
57
+
58
+ usage = compute_usage
59
+ usage > 0 ? remaining/usage : -1
60
+ end
61
+
62
+ def usage
63
+ @status.usage
64
+ end
65
+
66
+ def exceeded?
67
+ current_value > max_value
68
+ end
69
+
70
+ def authorized?
71
+ @status.authorized?
72
+ end
73
+
74
+ def inspect
75
+ "#<#{self.class.name} " \
76
+ "type=#{type} " \
77
+ "period=#{period} " \
78
+ "metric_name=#{metric_name} " \
79
+ "max_value=#{max_value} " \
80
+ "current_value=#{current_value}>"
81
+ end
82
+
83
+ def to_h
84
+ { period: period,
85
+ metric_name: metric_name,
86
+ max_value: max_value,
87
+ current_value: current_value }
88
+ end
89
+
90
+ def to_xml
91
+ xml = String.new
92
+ # Node header
93
+ add_head(xml)
94
+ # Node content
95
+ add_period(xml) if period != Period[:eternity]
96
+ add_values(xml)
97
+ # Node closing
98
+ add_tail(xml)
99
+ xml
100
+ end
101
+
102
+ private
103
+
104
+ def hierarchy
105
+ @status.hierarchy
106
+ end
107
+
108
+ def add_head(xml)
109
+ xml << '<usage_report metric="'.freeze
110
+ xml << metric_name.to_s << '" period="'.freeze
111
+ xml << period.to_s << '"'.freeze
112
+ xml << (exceeded? ? ' exceeded="true">'.freeze : '>'.freeze)
113
+ end
114
+
115
+ def add_period(xml)
116
+ xml << '<period_start>'.freeze
117
+ xml << period.start.strftime(TIME_FORMAT) << '</period_start>'.freeze
118
+ xml << '<period_end>'.freeze
119
+ xml << period.finish.strftime(TIME_FORMAT) << '</period_end>'.freeze
120
+ end
121
+
122
+ def add_values(xml)
123
+ xml << '<max_value>'.freeze
124
+ xml << max_value.to_s << '</max_value><current_value>'.freeze
125
+ xml << compute_current_value.to_s
126
+ xml << '</current_value>'
127
+ end
128
+
129
+ def add_tail(xml)
130
+ xml << '</usage_report>'.freeze
131
+ end
132
+
133
+ def remaining
134
+ # The remaining could be negative for several reasons:
135
+ # 1) We allow reports that do not check limits.
136
+ # 2) The reports included in authreps are async.
137
+ # 3) A usage passed by param in an authrep can go over the limits.
138
+ # However, a negative remaining does not make much sense. It's
139
+ # better to return just 0.
140
+ [max_value - compute_current_value, 0].max
141
+ end
142
+
143
+ def compute_usage
144
+ usage = @status.usage || @status.predicted_usage
145
+
146
+ return 0 unless usage
147
+
148
+ this_usage = usage[metric_name] || 0
149
+ res = Usage.get_from(this_usage)
150
+
151
+ add_descendants_usage(usage, res)
152
+ end
153
+
154
+ # helper to compute the current usage value after applying a possibly
155
+ # non-existent usage (or possibly unauthorized state)
156
+ def compute_current_value
157
+ # If not authorized or nothing to add, we just report the current
158
+ # value from the data store.
159
+ if authorized? && usage
160
+ this_usage = usage[metric_name] || 0
161
+ # this is an auth/authrep request and therefore we should sum the usage
162
+ computed_usage = Usage.get_from this_usage, current_value
163
+ # children can alter the resulting current value
164
+ add_descendants_usage(usage, computed_usage)
165
+ else
166
+ current_value
167
+ end
168
+ end
169
+
170
+ def add_descendants_usage(usages, parent_usage)
171
+ descendants = Metric.descendants(@status.service_id, metric_name)
172
+
173
+ descendants.reduce(parent_usage) do |acc, descendant|
174
+ descendant_usage = usages[descendant]
175
+ Usage.get_from descendant_usage, acc
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end