splitclient-rb 0.1.3
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 +7 -0
- data/.gitignore +38 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +152 -0
- data/Rakefile +4 -0
- data/lib/splitclient-cache/local_store.rb +45 -0
- data/lib/splitclient-engine/evaluator/splitter.rb +110 -0
- data/lib/splitclient-engine/impressions/impressions.rb +79 -0
- data/lib/splitclient-engine/matchers/all_keys_matcher.rb +46 -0
- data/lib/splitclient-engine/matchers/combiners.rb +13 -0
- data/lib/splitclient-engine/matchers/combining_matcher.rb +94 -0
- data/lib/splitclient-engine/matchers/negation_matcher.rb +54 -0
- data/lib/splitclient-engine/matchers/user_defined_segment_matcher.rb +58 -0
- data/lib/splitclient-engine/matchers/whitelist_matcher.rb +55 -0
- data/lib/splitclient-engine/metrics/binary_search_latency_tracker.rb +122 -0
- data/lib/splitclient-engine/metrics/metrics.rb +158 -0
- data/lib/splitclient-engine/parser/condition.rb +90 -0
- data/lib/splitclient-engine/parser/partition.rb +37 -0
- data/lib/splitclient-engine/parser/segment.rb +84 -0
- data/lib/splitclient-engine/parser/segment_parser.rb +46 -0
- data/lib/splitclient-engine/parser/split.rb +68 -0
- data/lib/splitclient-engine/parser/split_adapter.rb +433 -0
- data/lib/splitclient-engine/parser/split_parser.rb +129 -0
- data/lib/splitclient-engine/partitions/treatments.rb +40 -0
- data/lib/splitclient-rb.rb +22 -0
- data/lib/splitclient-rb/split_client.rb +170 -0
- data/lib/splitclient-rb/split_config.rb +193 -0
- data/lib/splitclient-rb/version.rb +3 -0
- data/splitclient-rb.gemspec +44 -0
- data/tasks/benchmark_is_treatment.rake +37 -0
- data/tasks/concurrent_benchmark_is_treatment.rake +43 -0
- data/tasks/console.rake +4 -0
- data/tasks/rspec.rake +3 -0
- metadata +260 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# acts as dto for a segment structure
|
|
5
|
+
#
|
|
6
|
+
class Segment < NoMethodError
|
|
7
|
+
#
|
|
8
|
+
# definition of the segment
|
|
9
|
+
#
|
|
10
|
+
# @returns [object] segment values
|
|
11
|
+
attr_accessor :data
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# users for the segment
|
|
15
|
+
#
|
|
16
|
+
# @returns [object] array of user keys
|
|
17
|
+
attr_accessor :users
|
|
18
|
+
|
|
19
|
+
#
|
|
20
|
+
# added users for the segment in a given time
|
|
21
|
+
#
|
|
22
|
+
# @returns [object] array of user keys that were added after the last segment fetch
|
|
23
|
+
attr_accessor :added
|
|
24
|
+
|
|
25
|
+
#
|
|
26
|
+
# removed users for the segment in a given time
|
|
27
|
+
#
|
|
28
|
+
# @returns [object] array of user keys that were removed after the last segment fetch
|
|
29
|
+
attr_accessor :removed
|
|
30
|
+
|
|
31
|
+
def initialize(segment)
|
|
32
|
+
@data = segment
|
|
33
|
+
@added = @data[:added]
|
|
34
|
+
@removed = @data[:removed]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# @returns [string] name of the segment
|
|
39
|
+
def name
|
|
40
|
+
@data[:name]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#
|
|
44
|
+
# @returns [int] since value fo the segment
|
|
45
|
+
def since
|
|
46
|
+
@data[:since]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
# @returns [int] till value fo the segment
|
|
51
|
+
def till
|
|
52
|
+
@data[:till]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# @return [boolean] true if the condition is empty false otherwise
|
|
57
|
+
def is_empty?
|
|
58
|
+
@data.empty? ? true : false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#
|
|
62
|
+
# updates the array of user keys valid for the segment, it's used after each segment fetch
|
|
63
|
+
#
|
|
64
|
+
# @param added [object] array of added user keys
|
|
65
|
+
# @param removed [object] array of removed user keys
|
|
66
|
+
#
|
|
67
|
+
# @return [void]
|
|
68
|
+
def refresh_users(added, removed)
|
|
69
|
+
if @users.nil?
|
|
70
|
+
@users = self.added
|
|
71
|
+
else
|
|
72
|
+
@added = added unless added.empty?
|
|
73
|
+
@removed = removed unless removed.empty?
|
|
74
|
+
self.removed.each do |r|
|
|
75
|
+
@users.delete_if { |u| u == r }
|
|
76
|
+
end
|
|
77
|
+
self.added.each do |a|
|
|
78
|
+
@users << a unless @users.include?(a)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
#
|
|
3
|
+
# helper class to parse fetched segments
|
|
4
|
+
#
|
|
5
|
+
class SegmentParser < NoMethodError
|
|
6
|
+
#
|
|
7
|
+
# segments data
|
|
8
|
+
attr_accessor :segments
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# since value for segments
|
|
12
|
+
attr_accessor :since
|
|
13
|
+
|
|
14
|
+
def initialize(logger)
|
|
15
|
+
@segments = []
|
|
16
|
+
@since = -1
|
|
17
|
+
@logger = logger
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# method to get a segment by name
|
|
22
|
+
#
|
|
23
|
+
# @param name [string] segment name
|
|
24
|
+
#
|
|
25
|
+
# @return [object] segment object
|
|
26
|
+
def get_segment(name)
|
|
27
|
+
@segments.find { |s| s.name == name }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
# method to get all segment names within the structure
|
|
32
|
+
#
|
|
33
|
+
# @return [object] array of segment names
|
|
34
|
+
def get_segment_names
|
|
35
|
+
@segments.map { |seg| seg.name }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# @return [boolean] true if the segment parser data is empty false otherwise
|
|
40
|
+
def is_empty?
|
|
41
|
+
@segments.empty? ? true : false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
#
|
|
3
|
+
# acts as dto for a split structure
|
|
4
|
+
#
|
|
5
|
+
class Split < NoMethodError
|
|
6
|
+
#
|
|
7
|
+
# definition of the split
|
|
8
|
+
#
|
|
9
|
+
# @returns [object] split values
|
|
10
|
+
attr_accessor :data
|
|
11
|
+
|
|
12
|
+
def initialize(split)
|
|
13
|
+
@data = split
|
|
14
|
+
@conditions = set_conditions
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# @returns [string] name of the split
|
|
19
|
+
def name
|
|
20
|
+
@data[:name]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#
|
|
24
|
+
# @returns [int] seed value of the split
|
|
25
|
+
def seed
|
|
26
|
+
@data[:seed]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# @returns [string] status value of the split
|
|
31
|
+
def status
|
|
32
|
+
@data[:status]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#
|
|
36
|
+
# @returns [string] killed value of the split
|
|
37
|
+
def killed?
|
|
38
|
+
@data[:killed]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# @returns [object] array of condition objects for this split
|
|
43
|
+
def conditions
|
|
44
|
+
@conditions
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#
|
|
48
|
+
# @return [boolean] true if the condition is empty false otherwise
|
|
49
|
+
def is_empty?
|
|
50
|
+
@data.empty? ? true : false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
# converts the conditions data into an array of condition objects for this split
|
|
55
|
+
#
|
|
56
|
+
# @return [object] array of condition objects
|
|
57
|
+
def set_conditions
|
|
58
|
+
conditions_list = []
|
|
59
|
+
@data[:conditions].each do |c|
|
|
60
|
+
condition = SplitIoClient::Condition.new(c)
|
|
61
|
+
conditions_list << condition
|
|
62
|
+
end
|
|
63
|
+
conditions_list
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'thread'
|
|
3
|
+
require 'faraday/http_cache'
|
|
4
|
+
require 'bundler/vendor/net/http/persistent'
|
|
5
|
+
require 'faraday_middleware'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
module SplitIoClient
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# acts as an api adapater to connect to split endpoints
|
|
12
|
+
# uses a configuration object that can be modified when creating the client instance
|
|
13
|
+
# also, uses safe threads to execute fetches and post give the time execution values from the config
|
|
14
|
+
#
|
|
15
|
+
class SplitAdapter < NoMethodError
|
|
16
|
+
#
|
|
17
|
+
# handler for impressions
|
|
18
|
+
attr_reader :impressions
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# handler for metrics
|
|
22
|
+
attr_reader :metrics
|
|
23
|
+
|
|
24
|
+
#
|
|
25
|
+
# handler for parsed splits
|
|
26
|
+
attr_reader :parsed_splits
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# handeler for parsed segments
|
|
30
|
+
attr_reader :parsed_segments
|
|
31
|
+
|
|
32
|
+
attr_reader :impressions_producer
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# Creates a new split api adapter instance that consumes split api endpoints
|
|
36
|
+
#
|
|
37
|
+
# @param api_key [String] the API key for your split account
|
|
38
|
+
#
|
|
39
|
+
# @return [SplitIoClient] split.io client instance
|
|
40
|
+
def initialize(api_key, config)
|
|
41
|
+
|
|
42
|
+
@api_key = api_key
|
|
43
|
+
@config = config
|
|
44
|
+
@parsed_splits = SplitParser.new(@config.logger)
|
|
45
|
+
@parsed_segments = SegmentParser.new(@config.logger)
|
|
46
|
+
@impressions = Impressions.new(100)
|
|
47
|
+
@metrics = Metrics.new(100)
|
|
48
|
+
|
|
49
|
+
@api_client = Faraday.new do |builder|
|
|
50
|
+
builder.use Faraday::HttpCache, store: @config.local_store
|
|
51
|
+
builder.use FaradayMiddleware::Gzip
|
|
52
|
+
builder.adapter :net_http_persistent
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@splits_consumer = create_splits_api_consumer
|
|
56
|
+
@segments_consumer = create_segments_api_consumer
|
|
57
|
+
@metrics_producer = create_metrics_api_producer
|
|
58
|
+
@impressions_producer = create_impressions_api_producer
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#
|
|
62
|
+
# creates a safe thread that will be executing api calls
|
|
63
|
+
# for fetching splits and segments give the execution time
|
|
64
|
+
# provided within the configuration
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
def create_splits_api_consumer
|
|
68
|
+
Thread.new do
|
|
69
|
+
loop do
|
|
70
|
+
begin
|
|
71
|
+
#splits fetcher
|
|
72
|
+
splits_arr = []
|
|
73
|
+
data = get_splits(@parsed_splits.since)
|
|
74
|
+
data[:splits].each do |split|
|
|
75
|
+
splits_arr << SplitIoClient::Split.new(split)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if @parsed_splits.is_empty?
|
|
79
|
+
@parsed_splits.splits = splits_arr
|
|
80
|
+
else
|
|
81
|
+
refresh_splits(splits_arr)
|
|
82
|
+
end
|
|
83
|
+
@parsed_splits.since = data[:till]
|
|
84
|
+
|
|
85
|
+
random_interval = randomize_interval @config.features_refresh_rate
|
|
86
|
+
sleep(random_interval)
|
|
87
|
+
rescue StandardError => error
|
|
88
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def create_segments_api_consumer
|
|
95
|
+
Thread.new do
|
|
96
|
+
loop do
|
|
97
|
+
begin
|
|
98
|
+
#segments fetcher
|
|
99
|
+
segments_arr = []
|
|
100
|
+
segment_data = get_segments(@parsed_splits.get_used_segments)
|
|
101
|
+
segment_data.each do |segment|
|
|
102
|
+
segments_arr << SplitIoClient::Segment.new(segment)
|
|
103
|
+
end
|
|
104
|
+
if @parsed_segments.is_empty?
|
|
105
|
+
@parsed_segments.segments = segments_arr
|
|
106
|
+
@parsed_segments.segments.map { |s| s.refresh_users(s.added, s.removed) }
|
|
107
|
+
else
|
|
108
|
+
refresh_segments(segments_arr)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
random_interval = randomize_interval @config.segments_refresh_rate
|
|
112
|
+
sleep(random_interval)
|
|
113
|
+
rescue StandardError => error
|
|
114
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
# helper method to execute a get request to the provided endpoint
|
|
122
|
+
#
|
|
123
|
+
# @param path [string] api endpoint path
|
|
124
|
+
# @param params [object] hash of params that will be added to the request
|
|
125
|
+
#
|
|
126
|
+
# @return [object] response to the request
|
|
127
|
+
def call_api(path, params = {})
|
|
128
|
+
@api_client.get @config.base_uri + path, params do |req|
|
|
129
|
+
req.headers['Authorization'] = 'Bearer ' + @api_key
|
|
130
|
+
req.headers['SplitSDKVersion'] = SplitIoClient::SplitClient.sdk_version
|
|
131
|
+
req.headers['SplitSDKMachineName'] = @config.machine_name
|
|
132
|
+
req.headers['SplitSDKMachineIP'] = @config.machine_ip
|
|
133
|
+
req.headers['Accept-Encoding'] = 'gzip'
|
|
134
|
+
req.options.open_timeout = @config.connection_timeout
|
|
135
|
+
req.options.timeout = @config.read_timeout
|
|
136
|
+
@config.logger.debug("GET #{@config.base_uri + path}") if @config.debug_enabled
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#
|
|
141
|
+
# helper method to execute a post request to the provided endpoint
|
|
142
|
+
#
|
|
143
|
+
# @param path [string] api endpoint path
|
|
144
|
+
# @param params [object] hash of params that will be added to the request
|
|
145
|
+
#
|
|
146
|
+
# @return [object] response to the request
|
|
147
|
+
def post_api(path, param)
|
|
148
|
+
@api_client.post (@config.base_uri + path) do |req|
|
|
149
|
+
req.headers['Authorization'] = 'Bearer ' + @api_key
|
|
150
|
+
req.headers['Content-Type'] = 'application/json'
|
|
151
|
+
req.headers['SplitSDKVersion'] = SplitIoClient::SplitClient.sdk_version
|
|
152
|
+
req.headers['SplitSDKMachineName'] = @config.machine_name
|
|
153
|
+
req.headers['SplitSDKMachineIP'] = @config.machine_ip
|
|
154
|
+
req.body = param.to_json
|
|
155
|
+
req.options.timeout = @config.read_timeout
|
|
156
|
+
req.options.open_timeout = @config.connection_timeout
|
|
157
|
+
@config.logger.debug("POST #{@config.base_uri + path} #{req.body}") if @config.debug_enabled
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
#
|
|
162
|
+
# helper method to fetch splits by using the appropriate api endpoint
|
|
163
|
+
#
|
|
164
|
+
# @param since [int] since value for the last fetch
|
|
165
|
+
#
|
|
166
|
+
# @return splits [object] splits structure in json format
|
|
167
|
+
def get_splits(since)
|
|
168
|
+
result = nil
|
|
169
|
+
start = Time.now
|
|
170
|
+
prefix = 'splitChangeFetcher'
|
|
171
|
+
|
|
172
|
+
splits = call_api('/splitChanges', {:since => since})
|
|
173
|
+
|
|
174
|
+
if splits.status / 100 == 2
|
|
175
|
+
result = JSON.parse(splits.body, symbolize_names: true)
|
|
176
|
+
@metrics.count(prefix + '.status.' + splits.status.to_s, 1)
|
|
177
|
+
@config.logger.info("#{result[:splits].length} splits retrieved.")
|
|
178
|
+
@config.logger.debug("#{result}") if @config.debug_enabled
|
|
179
|
+
else
|
|
180
|
+
@config.logger.error('Unexpected result from API call')
|
|
181
|
+
@metrics.count(prefix + '.status.' + splits.status.to_s, 1)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
latency = (Time.now - start) * 1000.0
|
|
185
|
+
@metrics.time(prefix + '.time', latency)
|
|
186
|
+
|
|
187
|
+
result
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
#
|
|
191
|
+
# helper method to refresh splits values after a new fetch with changes
|
|
192
|
+
#
|
|
193
|
+
# @param splits_arr [object] array of splits to refresh
|
|
194
|
+
#
|
|
195
|
+
# @return [void]
|
|
196
|
+
def refresh_splits(splits_arr)
|
|
197
|
+
feature_names = splits_arr.map { |s| s.name }
|
|
198
|
+
@parsed_splits.splits.delete_if { |sp| feature_names.include?(sp.name) }
|
|
199
|
+
@parsed_splits.splits += splits_arr
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
#
|
|
203
|
+
# helper method to fetch segments by using the appropriate api endpoint
|
|
204
|
+
#
|
|
205
|
+
# @param names [object] array of segment names that must be fetched
|
|
206
|
+
#
|
|
207
|
+
# @return segments [object] segments structure in json format
|
|
208
|
+
def get_segments(names)
|
|
209
|
+
segments = []
|
|
210
|
+
start = Time.now
|
|
211
|
+
prefix = 'segmentChangeFetcher'
|
|
212
|
+
|
|
213
|
+
names.each do |name|
|
|
214
|
+
curr_segment = @parsed_segments.get_segment(name)
|
|
215
|
+
since = curr_segment.nil? ? -1 : curr_segment.till
|
|
216
|
+
|
|
217
|
+
while true
|
|
218
|
+
segment = call_api('/segmentChanges/' + name, {:since => since})
|
|
219
|
+
|
|
220
|
+
if segment.status / 100 == 2
|
|
221
|
+
segment_content = JSON.parse(segment.body, symbolize_names: true)
|
|
222
|
+
@parsed_segments.since = segment_content[:till]
|
|
223
|
+
@metrics.count(prefix + '.status.' + segment.status.to_s, 1)
|
|
224
|
+
@config.logger.info("\'#{segment_content[:name]}\' segment retrieved.")
|
|
225
|
+
@config.logger.debug("#{segment_content}") if @config.debug_enabled
|
|
226
|
+
segments << segment_content
|
|
227
|
+
else
|
|
228
|
+
@config.logger.error('Unexpected result from API call')
|
|
229
|
+
@metrics.count(prefix + '.status.' + segment.status.to_s, 1)
|
|
230
|
+
end
|
|
231
|
+
break if (since.to_i >= @parsed_segments.since.to_i)
|
|
232
|
+
since = @parsed_segments.since
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
latency = (Time.now - start) * 1000.0
|
|
237
|
+
@metrics.time(prefix + '.time', latency)
|
|
238
|
+
|
|
239
|
+
segments
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
#
|
|
243
|
+
# helper method to refresh segments values after a new fetch with changes
|
|
244
|
+
#
|
|
245
|
+
# @param segments_arr [object] array of segments to refresh
|
|
246
|
+
#
|
|
247
|
+
# @return [void]
|
|
248
|
+
def refresh_segments(segments_arr)
|
|
249
|
+
segment_names = @parsed_segments.segments.map { |s| s.name }
|
|
250
|
+
segments_arr.each do |s|
|
|
251
|
+
if segment_names.include?(s.name)
|
|
252
|
+
segment_to_update = @parsed_segments.get_segment(s.name)
|
|
253
|
+
segment_to_update.refresh_users(s.added, s.removed)
|
|
254
|
+
else
|
|
255
|
+
@parsed_segments.segments << s
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
#
|
|
261
|
+
# @return parsed_splits [object] parsed splits for this adapter
|
|
262
|
+
def parsed_splits
|
|
263
|
+
@parsed_splits
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
#
|
|
267
|
+
# @return parsed_segments [object] parsed segments for this adapter
|
|
268
|
+
def parsed_segments
|
|
269
|
+
@parsed_segments
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
#
|
|
273
|
+
# creates two safe threads that will be executing api calls
|
|
274
|
+
# for posting impressions and metrics given the execution time
|
|
275
|
+
# provided within the configuration
|
|
276
|
+
#
|
|
277
|
+
|
|
278
|
+
def create_metrics_api_producer
|
|
279
|
+
Thread.new do
|
|
280
|
+
loop do
|
|
281
|
+
begin
|
|
282
|
+
#post captured metrics
|
|
283
|
+
post_metrics
|
|
284
|
+
|
|
285
|
+
random_interval = randomize_interval @config.metrics_refresh_rate
|
|
286
|
+
sleep(random_interval)
|
|
287
|
+
rescue StandardError => error
|
|
288
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def create_impressions_api_producer
|
|
295
|
+
Thread.new do
|
|
296
|
+
loop do
|
|
297
|
+
begin
|
|
298
|
+
#post captured impressions
|
|
299
|
+
post_impressions
|
|
300
|
+
|
|
301
|
+
random_interval = randomize_interval @config.impressions_refresh_rate
|
|
302
|
+
sleep(random_interval)
|
|
303
|
+
rescue StandardError => error
|
|
304
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
#
|
|
311
|
+
# creates the appropriate json data for the cached impressions values
|
|
312
|
+
# and then sends them to the appropriate api endpoint with a valid body format
|
|
313
|
+
#
|
|
314
|
+
# @return [void]
|
|
315
|
+
def post_impressions
|
|
316
|
+
if @impressions.queue.empty?
|
|
317
|
+
@config.logger.info('No impressions to report.')
|
|
318
|
+
else
|
|
319
|
+
popped_impressions = @impressions.clear
|
|
320
|
+
test_impression_array = []
|
|
321
|
+
popped_impressions.each do |i|
|
|
322
|
+
filtered = []
|
|
323
|
+
keys_seen = []
|
|
324
|
+
|
|
325
|
+
impressions = i[:impressions]
|
|
326
|
+
impressions.each do |imp|
|
|
327
|
+
if keys_seen.include?(imp.key)
|
|
328
|
+
next
|
|
329
|
+
end
|
|
330
|
+
keys_seen << imp.key
|
|
331
|
+
filtered << imp
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
if filtered.empty?
|
|
335
|
+
@config.logger.info('No impressions to report post filtering.')
|
|
336
|
+
else
|
|
337
|
+
test_impression = {}
|
|
338
|
+
key_impressions = []
|
|
339
|
+
|
|
340
|
+
filtered.each do |f|
|
|
341
|
+
key_impressions << {keyName: f.key, treatment: f.treatment, time: f.time.to_i}
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
test_impression = {testName: i[:feature], keyImpressions: key_impressions}
|
|
345
|
+
test_impression_array << test_impression
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
res = post_api('/testImpressions/bulk', test_impression_array)
|
|
350
|
+
if res.status / 100 != 2
|
|
351
|
+
@config.logger.error("Unexpected status code while posting impressions: #{res.status}")
|
|
352
|
+
else
|
|
353
|
+
@config.logger.info("Impressions reported.")
|
|
354
|
+
@config.logger.debug("#{test_impression_array}")if @config.debug_enabled
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
#
|
|
360
|
+
# creates the appropriate json data for the cached metrics values
|
|
361
|
+
# include latencies, counts and gauges
|
|
362
|
+
# and then sends them to the appropriate api endpoint with a valida body format
|
|
363
|
+
#
|
|
364
|
+
# @return [void]
|
|
365
|
+
def post_metrics
|
|
366
|
+
clear = true
|
|
367
|
+
if @metrics.latencies.empty?
|
|
368
|
+
@config.logger.info('No latencies to report.')
|
|
369
|
+
else
|
|
370
|
+
@metrics.latencies.each do |l|
|
|
371
|
+
metrics_time = {}
|
|
372
|
+
metrics_time = {name: l[:operation], latencies: l[:latencies]}
|
|
373
|
+
res = post_api('/metrics/time', metrics_time)
|
|
374
|
+
if res.status / 100 != 2
|
|
375
|
+
@config.logger.error("Unexpected status code while posting time metrics: #{res.status}")
|
|
376
|
+
clear = false
|
|
377
|
+
else
|
|
378
|
+
@config.logger.info("Metric time reported.")
|
|
379
|
+
@config.logger.debug("#{metrics_time}") if @config.debug_enabled
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
@metrics.latencies.clear if clear
|
|
384
|
+
|
|
385
|
+
clear = true
|
|
386
|
+
if @metrics.counts.empty?
|
|
387
|
+
@config.logger.info('No counts to report.')
|
|
388
|
+
else
|
|
389
|
+
@metrics.counts.each do |c|
|
|
390
|
+
metrics_count = {}
|
|
391
|
+
metrics_count = {name: c[:name], delta: c[:delta].sum}
|
|
392
|
+
res = post_api('/metrics/counter', metrics_count)
|
|
393
|
+
if res.status / 100 != 2
|
|
394
|
+
@config.logger.error("Unexpected status code while posting count metrics: #{res.status}")
|
|
395
|
+
clear = false
|
|
396
|
+
else
|
|
397
|
+
@config.logger.info("Metric counts reported.")
|
|
398
|
+
@config.logger.debug("#{metrics_count}") if @config.debug_enabled
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
@metrics.counts.clear if clear
|
|
403
|
+
|
|
404
|
+
clear = true
|
|
405
|
+
if @metrics.gauges.empty?
|
|
406
|
+
@config.logger.info('No gauges to report.')
|
|
407
|
+
else
|
|
408
|
+
@metrics.gauges.each do |g|
|
|
409
|
+
metrics_gauge = {}
|
|
410
|
+
metrics_gauge = {name: g[:name], value: g[:gauge].value}
|
|
411
|
+
res = post_api('/metrics/gauge', metrics_gauge)
|
|
412
|
+
if res.status / 100 != 2
|
|
413
|
+
@config.logger.error("Unexpected status code while posting gauge metrics: #{res.status}")
|
|
414
|
+
clear = false
|
|
415
|
+
else
|
|
416
|
+
@config.logger.info("Metric gauge reported.")
|
|
417
|
+
@config.logger.debug("#{metrics_gauge}") if @config.debug_enabled
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
@metrics.gauges.clear if clear
|
|
422
|
+
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
private
|
|
426
|
+
|
|
427
|
+
def randomize_interval(interval)
|
|
428
|
+
@random_generator ||= Random.new
|
|
429
|
+
random_factor = @random_generator.rand(50..100)/100.0
|
|
430
|
+
interval * random_factor
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|