apisonator 2.100.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.
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,97 @@
1
+ module ThreeScale
2
+ module Backend
3
+ class ServiceToken
4
+
5
+ module KeyHelpers
6
+ def key(service_token, service_id)
7
+ encode_key("service_token/token:#{service_token}/service_id:#{service_id}")
8
+ end
9
+ end
10
+
11
+ include KeyHelpers
12
+ extend KeyHelpers
13
+ include Storable
14
+
15
+ ValidationError = Class.new(ThreeScale::Backend::Invalid)
16
+
17
+ class InvalidServiceToken < ValidationError
18
+ def initialize
19
+ super('Service token cannot be blank'.freeze)
20
+ end
21
+ end
22
+
23
+ class InvalidServiceId < ValidationError
24
+ def initialize
25
+ super('Service ID cannot be blank'.freeze)
26
+ end
27
+ end
28
+
29
+ # We want to use a hash in Redis because in the future we might have
30
+ # several fields related to permissions and roles.
31
+ # For now we do not need any of those fields, but we need to define at
32
+ # least one to be able to create a hash, even if we are not going to use
33
+ # it.
34
+ PERMISSIONS_KEY_FIELD = 'permissions'.freeze
35
+ private_constant :PERMISSIONS_KEY_FIELD
36
+
37
+ class << self
38
+ include Memoizer::Decorator
39
+
40
+ def save(service_token, service_id)
41
+ validate_pairs([{ service_token: service_token, service_id: service_id }])
42
+ storage.hset(key(service_token, service_id), PERMISSIONS_KEY_FIELD, ''.freeze)
43
+ end
44
+
45
+ # Saves a collection of (service_token, service_id) pairs only if all
46
+ # the pairs contain valid data, meaning that there are no null or empty
47
+ # strings.
48
+ def save_pairs(token_id_pairs)
49
+ validate_pairs(token_id_pairs)
50
+
51
+ token_id_pairs.each do |pair|
52
+ unchecked_save(pair[:service_token], pair[:service_id])
53
+ end
54
+ end
55
+
56
+ def delete(service_token, service_id)
57
+ res = storage.del(key(service_token, service_id))
58
+ clear_cache(service_token, service_id)
59
+ res
60
+ end
61
+
62
+ def exists?(service_token, service_id)
63
+ storage.exists(key(service_token, service_id))
64
+ end
65
+ memoize :exists?
66
+
67
+ private
68
+
69
+ def validate_pairs(token_id_pairs)
70
+ invalid_token = token_id_pairs.any? do |pair|
71
+ pair[:service_token].nil? || pair[:service_token].empty?
72
+ end
73
+ raise InvalidServiceToken if invalid_token
74
+
75
+ invalid_service_id = token_id_pairs.any? do |pair|
76
+ pair[:service_id].nil? || pair[:service_id].to_s.empty?
77
+ end
78
+ raise InvalidServiceId if invalid_service_id
79
+ end
80
+
81
+ def unchecked_save(service_token, service_id)
82
+ storage.hset(key(service_token, service_id), PERMISSIONS_KEY_FIELD, ''.freeze)
83
+ end
84
+
85
+ def storage
86
+ Storage.instance
87
+ end
88
+
89
+ def clear_cache(service_token, service_id)
90
+ Memoizer.clear(Memoizer.build_key(self, :exists?, service_token, service_id))
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,8 @@
1
+ require '3scale/backend/stats/codes_commons'
2
+ require '3scale/backend/stats/period_commons'
3
+ require '3scale/backend/stats/aggregator'
4
+ require '3scale/backend/stats/delete_job_def'
5
+ require '3scale/backend/stats/key_generator'
6
+ require '3scale/backend/stats/partition_generator_job'
7
+ require '3scale/backend/stats/partition_eraser_job'
8
+ require '3scale/backend/stats/cleaner'
@@ -0,0 +1,170 @@
1
+ require '3scale/backend/logging'
2
+ require '3scale/backend/stats/storage'
3
+ require '3scale/backend/stats/keys'
4
+ require '3scale/backend/application_events'
5
+ require '3scale/backend/transaction'
6
+ require '3scale/backend/stats/aggregators/response_code'
7
+ require '3scale/backend/stats/aggregators/usage'
8
+
9
+ module ThreeScale
10
+ module Backend
11
+ module Stats
12
+
13
+ # This class contains several methods that deal with buckets, which are
14
+ # only used in the SaaS analytics system.
15
+ class Aggregator
16
+ # We need to limit the number of buckets stored in the system.
17
+ # The reason is that our Redis can grow VERY quickly if we start
18
+ # creating buckets and we never delete them.
19
+ # When the max defined is reached, I simply disable the option
20
+ # to save the stats keys in buckets. Yes, we will lose data,
21
+ # but that is better than the alternative. We will try to find
22
+ # a better alternative once we cannot afford to lose data.
23
+ # Right now, we are just deleting the stats keys with
24
+ # period = minute, so we can restore everything else.
25
+ MAX_BUCKETS = 360
26
+ private_constant :MAX_BUCKETS
27
+
28
+ MAX_BUCKETS_CREATED_MSG =
29
+ 'Bucket creation has been disabled. Max number of stats buckets reached'.freeze
30
+ private_constant :MAX_BUCKETS_CREATED_MSG
31
+
32
+ class << self
33
+ include Backend::StorageKeyHelpers
34
+ include Configurable
35
+ include Keys
36
+ include Logging
37
+
38
+ # This method stores the events in buckets if that option is enabled
39
+ # or if it was disable because of an emergency (not because a user
40
+ # did it manually), and Kinesis has already consumed all the pending
41
+ # buckets.
42
+ def process(transactions)
43
+ current_bucket = nil
44
+
45
+ if configuration.can_create_event_buckets
46
+ # Only disable indicating emergency if bucket storage is enabled.
47
+ # Otherwise, we might indicate emergency when a user manually
48
+ # disabled it previously.
49
+ if Storage.enabled? && buckets_limit_exceeded?
50
+ Storage.disable!(true)
51
+ log_bucket_creation_disabled
52
+ elsif save_in_bucket?
53
+ Storage.enable! unless Storage.enabled?
54
+ current_bucket = Time.now.utc.beginning_of_bucket(stats_bucket_size)
55
+ .to_not_compact_s
56
+ end
57
+ end
58
+
59
+ touched_apps = aggregate(transactions, current_bucket)
60
+
61
+ ApplicationEvents.generate(touched_apps.values)
62
+ update_alerts(touched_apps)
63
+ begin
64
+ ApplicationEvents.ping
65
+ rescue ApplicationEvents::PingFailed => e
66
+ # we could not ping the frontend, log it
67
+ logger.notify e
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ # Aggregate stats values for a collection of Transactions.
74
+ #
75
+ # @param [Array] transactions the collection of transactions
76
+ # @param [String, Nil] bucket
77
+ # @return [Hash] A Hash where each key is an application_id and the
78
+ # value is another Hash with service_id and application_id.
79
+ def aggregate(transactions, bucket = nil)
80
+ touched_apps = {}
81
+
82
+ transactions.each_slice(PIPELINED_SLICE_SIZE) do |slice|
83
+ storage.pipelined do
84
+ slice.each do |transaction|
85
+ aggregate_all(transaction, bucket)
86
+ touched_apps.merge!(touched_relation(transaction))
87
+ end
88
+ end
89
+ end
90
+
91
+ touched_apps
92
+ end
93
+
94
+ def aggregate_all(transaction, bucket)
95
+ [Aggregators::ResponseCode, Aggregators::Usage].each do |aggregator|
96
+ aggregator.aggregate(transaction, bucket)
97
+ end
98
+ end
99
+
100
+ def save_in_bucket?
101
+ if Storage.enabled?
102
+ true
103
+ else
104
+ Storage.last_disable_was_emergency? && bucket_storage.pending_buckets_size == 0
105
+ end
106
+ end
107
+
108
+ def stats_bucket_size
109
+ @stats_bucket_size ||= (configuration.stats.bucket_size || 5)
110
+ end
111
+
112
+ def storage
113
+ Backend::Storage.instance
114
+ end
115
+
116
+ def bucket_storage
117
+ Stats::Storage.bucket_storage
118
+ end
119
+
120
+ # Return a Hash with needed info to update usages and alerts.
121
+ #
122
+ # @param [Transaction] transaction
123
+ # @return [Hash] the hash that contains the application_id that has
124
+ # been updated and the transaction's service_id. The key of the
125
+ # hash is the application_id.
126
+ def touched_relation(transaction)
127
+ relation_value = transaction.send(:application_id)
128
+ { relation_value => { application_id: relation_value,
129
+ service_id: transaction.service_id } }
130
+ end
131
+
132
+ def buckets_limit_exceeded?
133
+ bucket_storage.pending_buckets_size > MAX_BUCKETS
134
+ end
135
+
136
+ def log_bucket_creation_disabled
137
+ logger.info(MAX_BUCKETS_CREATED_MSG)
138
+ end
139
+
140
+ def update_alerts(applications)
141
+ current_timestamp = Time.now.getutc
142
+
143
+ applications.each do |_appid, values|
144
+ service_id = values[:service_id]
145
+ application = Backend::Application.load(service_id,
146
+ values[:application_id])
147
+
148
+ application.load_metric_names
149
+ usage = Usage.application_usage(application, current_timestamp)
150
+ status = Transactor::Status.new(service_id: service_id,
151
+ application: application,
152
+ values: usage)
153
+
154
+ max_utilization, max_record = Alerts.utilization(
155
+ status.application_usage_reports)
156
+
157
+ if max_utilization >= 0.0
158
+ Alerts.update_utilization(service_id,
159
+ values[:application_id],
160
+ max_utilization,
161
+ max_record,
162
+ current_timestamp)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,72 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module Stats
4
+ module Aggregators
5
+ module Base
6
+ # Aggregates a value in a timestamp for all given keys using a specific
7
+ # Redis command to store them. If a bucket_key is specified, each key will
8
+ # be added to a Redis Set with that name.
9
+ #
10
+ # @param [Integer] value
11
+ # @param [Time] timestamp
12
+ # @param [Array] keys array of {(service|application|user) => "key"}
13
+ # @param [Symbol] cmd
14
+ # @param [String, Nil] bucket
15
+ def aggregate_values(value, timestamp, keys, cmd, bucket)
16
+ keys_for_bucket = []
17
+
18
+ keys.each do |metric_type, prefix_key|
19
+ granularities(metric_type).each do |granularity|
20
+ key = counter_key(prefix_key, granularity.new(timestamp))
21
+ expire_time = Stats::PeriodCommons.expire_time_for_granularity(granularity)
22
+
23
+ store_key(cmd, key, value, expire_time)
24
+
25
+ unless Stats::PeriodCommons::EXCLUDED_FOR_BUCKETS.include?(granularity)
26
+ keys_for_bucket << key
27
+ end
28
+ end
29
+ end
30
+
31
+ store_in_changed_keys(keys_for_bucket, bucket) if bucket
32
+ end
33
+
34
+ # Return Redis command depending on raw_value.
35
+ # If raw_value is a string with a '#' in the beginning, it returns 'set'.
36
+ # Else, it returns 'incrby'.
37
+ #
38
+ # @param [String] raw_value
39
+ # @return [Symbol] the Redis command
40
+ def storage_cmd(raw_value)
41
+ Backend::Usage.is_set?(raw_value) ? :set : :incrby
42
+ end
43
+
44
+ def storage
45
+ Backend::Storage.instance
46
+ end
47
+
48
+ protected
49
+
50
+ def granularities(metric_type)
51
+ metric_type == :service ? Stats::PeriodCommons::SERVICE_GRANULARITIES : Stats::PeriodCommons::EXPANDED_GRANULARITIES
52
+ end
53
+
54
+ def store_key(cmd, key, value, expire_time = nil)
55
+ storage.send(cmd, key, value)
56
+ storage.expire(key, expire_time) if expire_time
57
+ end
58
+
59
+ def store_in_changed_keys(keys, bucket)
60
+ bucket_storage.put_in_bucket(keys, bucket)
61
+ end
62
+
63
+ private
64
+
65
+ def bucket_storage
66
+ Stats::Storage.bucket_storage
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,58 @@
1
+ require '3scale/backend/stats/keys'
2
+ require '3scale/backend/transaction'
3
+ require '3scale/backend/stats/aggregators/base'
4
+
5
+ module ThreeScale
6
+ module Backend
7
+ module Stats
8
+ module Aggregators
9
+ class ResponseCode
10
+ class << self
11
+ include Keys
12
+ include Base
13
+
14
+ def aggregate(transaction, bucket = nil)
15
+ keys_for_multiple_codes = keys_for_response_code(transaction)
16
+ timestamp = transaction.timestamp
17
+
18
+ # For now, we are not interested in storing response codes in
19
+ # buckets, that is the reason why set bucket to nil.
20
+ bucket = nil
21
+
22
+ keys_for_multiple_codes.each do |keys|
23
+ aggregate_values(1, timestamp, keys, :incrby, bucket)
24
+ end
25
+ end
26
+
27
+
28
+ protected
29
+ def keys_for_response_code(transaction)
30
+ response_code = transaction.extract_response_code
31
+ return {} unless response_code
32
+ values = values_to_inc(response_code)
33
+ values.flat_map do |code|
34
+ Keys.transaction_keys(transaction, :response_code, code)
35
+ end
36
+ end
37
+
38
+ def values_to_inc(response_code)
39
+ group_code = Stats::CodesCommons.get_http_code_group(response_code)
40
+ [].tap do |keys|
41
+ keys << group_code if tracked_group_code?(group_code)
42
+ keys << response_code.to_s if tracked_code?(response_code)
43
+ end
44
+ end
45
+
46
+ def tracked_code?(code)
47
+ Stats::CodesCommons::TRACKED_CODES.include?(code)
48
+ end
49
+
50
+ def tracked_group_code?(group_code)
51
+ Stats::CodesCommons::TRACKED_CODE_GROUPS.include?(group_code)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ require '3scale/backend/stats/keys'
2
+ require '3scale/backend/transaction'
3
+ require '3scale/backend/stats/aggregators/base'
4
+
5
+ module ThreeScale
6
+ module Backend
7
+ module Stats
8
+ module Aggregators
9
+ class Usage
10
+ class << self
11
+ include Keys
12
+ include Base
13
+
14
+ # Aggregates the usage of a transaction. If a bucket time is specified,
15
+ # all new or updated stats keys will be stored in a Redis Set.
16
+ #
17
+ # @param [Transaction] transaction
18
+ # @param [String, Nil] bucket
19
+ def aggregate(transaction, bucket = nil)
20
+ transaction.usage.each do |metric_id, raw_value|
21
+ metric_keys = Keys.transaction_keys(transaction, :metric, metric_id)
22
+ cmd = storage_cmd(raw_value)
23
+ value = Backend::Usage.get_from raw_value
24
+
25
+ aggregate_values(value, transaction.timestamp, metric_keys, cmd, bucket)
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,135 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module Stats
4
+
5
+ # This class allows us to read the buckets that we are creating in Redis
6
+ # to store the stats keys that change. It also allows us to keep track of
7
+ # the ones that are pending to be read.
8
+ class BucketReader
9
+
10
+ # This private nested class allows us to isolate accesses to Redis.
11
+ class LatestBucketReadMarker
12
+ LATEST_BUCKET_READ_KEY = 'send_to_kinesis:latest_bucket_read'
13
+ private_constant :LATEST_BUCKET_READ_KEY
14
+
15
+ def initialize(storage)
16
+ @storage = storage
17
+ end
18
+
19
+ def latest_bucket_read=(latest_bucket_read)
20
+ storage.set(LATEST_BUCKET_READ_KEY, latest_bucket_read)
21
+ end
22
+
23
+ def latest_bucket_read
24
+ storage.get(LATEST_BUCKET_READ_KEY)
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :storage
30
+ end
31
+ private_constant :LatestBucketReadMarker
32
+
33
+ # Before we read and mark a bucket as read, we need to make sure that
34
+ # it will not receive more events. Otherwise, there is the risk that
35
+ # we will miss some events.
36
+ # Buckets are created every 'bucket_create_interval' seconds, it is one
37
+ # of the parameters that 'initialize' receives. We should be able to
38
+ # read any bucket identified with a timestamp ts, where
39
+ # ts < Time.now - bucket_create_interval. However, in order to be sure
40
+ # that we will not miss any events, we are going to define a constant
41
+ # that will define some backup time.
42
+ BACKUP_SECONDS_READ_BUCKET = 10
43
+ private_constant :BACKUP_SECONDS_READ_BUCKET
44
+
45
+ KEYS_SLICE_CALL_TO_REDIS = 1000
46
+ private_constant :KEYS_SLICE_CALL_TO_REDIS
47
+
48
+ InvalidInterval = Class.new(ThreeScale::Backend::Error)
49
+
50
+ def initialize(bucket_create_interval, bucket_storage, events_storage)
51
+ # This is needed because ThreeScale::Backend::TimeHacks.beginning_of_bucket
52
+ if 60%bucket_create_interval != 0 || bucket_create_interval <= 0
53
+ raise InvalidInterval, 'Bucket create interval needs to divide 60'
54
+ end
55
+
56
+ @bucket_create_interval = bucket_create_interval
57
+ @bucket_storage = bucket_storage
58
+ @events_storage = events_storage
59
+ @latest_bucket_read_marker = LatestBucketReadMarker.new(bucket_storage.storage)
60
+ end
61
+
62
+ # Returns the pending events and the bucket of the most recent of the
63
+ # events sent. This allows the caller to call latest_bucket_read= when
64
+ # it has processed all the events.
65
+ def pending_events_in_buckets(end_time_utc: Time.now.utc, max_buckets: nil)
66
+ buckets = if max_buckets
67
+ pending_buckets(end_time_utc).take(max_buckets)
68
+ else
69
+ pending_buckets(end_time_utc).to_a
70
+ end
71
+
72
+ { events: events(buckets), latest_bucket: buckets.last }
73
+ end
74
+
75
+ def latest_bucket_read=(latest_bucket_read)
76
+ latest_bucket_read_marker.latest_bucket_read = latest_bucket_read
77
+ end
78
+
79
+ private
80
+
81
+ attr_reader :bucket_create_interval,
82
+ :bucket_storage,
83
+ :events_storage,
84
+ :latest_bucket_read_marker
85
+
86
+ def pending_buckets(end_time_utc = Time.now.utc)
87
+ latest_bucket_read = latest_bucket_read_marker.latest_bucket_read
88
+ start_time = unless latest_bucket_read.nil?
89
+ bucket_to_time(latest_bucket_read) + bucket_create_interval
90
+ end
91
+ end_time = end_time_with_backup(end_time_utc)
92
+ stored_buckets(start_time, end_time)
93
+ end
94
+
95
+ def events(buckets)
96
+ event_keys = bucket_storage.content(buckets)
97
+
98
+ # Values are stored as strings in Redis, but we want integers.
99
+ # There are some values that can be nil. This happens when the key
100
+ # has a TTL and we read it once it has expired. Right now, event keys
101
+ # with granularity = 'minute' expire after 180 s (see
102
+ # Stats::Aggregators::Base module). We might need to increase that to
103
+ # make sure that we do not miss any values.
104
+ event_values = event_keys.each_slice(KEYS_SLICE_CALL_TO_REDIS)
105
+ .flat_map do |keys_slice|
106
+ events_storage.mget(keys_slice)
107
+ end.map { |value| Integer(value) if value }
108
+
109
+ Hash[event_keys.zip(event_values)]
110
+ end
111
+
112
+ def end_time_with_backup(end_time_utc)
113
+ [end_time_utc, Time.now.utc - bucket_create_interval - BACKUP_SECONDS_READ_BUCKET].min
114
+ end
115
+
116
+ def stored_buckets(start_time_utc, end_time_utc)
117
+ range = { }
118
+ range[:first] = time_to_bucket_name(start_time_utc) if start_time_utc
119
+ range[:last] = time_to_bucket_name(end_time_utc)
120
+ bucket_storage.buckets(range)
121
+ end
122
+
123
+ def time_to_bucket_name(time_utc)
124
+ # We know that the names of the buckets follow the following pattern:
125
+ # they are a timestamp with format YYYYmmddHHMMSS
126
+ time_utc.strftime('%Y%m%d%H%M%S')
127
+ end
128
+
129
+ def bucket_to_time(bucket_name)
130
+ DateTime.parse(bucket_name).to_time.utc
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end