flagger 2.0.5 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,440 +0,0 @@
1
- require 'eventmachine'
2
- require 'faraday'
3
- require 'faye/websocket'
4
- require 'json'
5
- require 'lru_redux'
6
- require 'set'
7
- require 'flagger/models'
8
- require 'flagger/stat'
9
- require 'flagger/version'
10
-
11
- module FlaggerEnvironments
12
- SERVER_URL = 'https://api.airshiphq.com'
13
- IDENTIFY_ENDPOINT = "#{SERVER_URL}/v2/identify"
14
- GATING_INFO_ENDPOINT = "#{SERVER_URL}/v2/gating-info"
15
-
16
- WEBSOCKET_URL = 'wss://ws.airshiphq.com'
17
- WEBSOCKET_GATING_INFO_ENDPOINT = "#{WEBSOCKET_URL}/v2/ws-events"
18
-
19
- CLOUDFRONT_URL = 'https://backup-api.airshiphq.com'
20
- CLOUDFRONT_GATING_INFO_ENDPOINT = "#{CLOUDFRONT_URL}/v2/gating-info"
21
-
22
- class CloudDelegate
23
- attr_accessor :gating_info
24
- attr_accessor :env_key
25
- attr_accessor :request_timeout
26
-
27
- def initialize(env_key, request_timeout)
28
- @env_key = env_key
29
- @request_timeout = request_timeout
30
-
31
- @ingestion_max_items = 500
32
- @ingestion_interval = 30
33
-
34
- @entities = []
35
- @stats = []
36
- @exposures = []
37
- @flags = Set[]
38
- @ingested_flags = Set[]
39
-
40
- @entity_lru_hashes = LruRedux::Cache.new(500)
41
- @first_ingestion = true
42
-
43
- @should_ingest_entities = true
44
- @should_ingest_stats = true
45
- @should_ingest_exposures = true
46
- @should_ingest_flags = true
47
-
48
- begin
49
- get_gating_info("#{GATING_INFO_ENDPOINT}/#{@env_key}", 'duration__gating_info')
50
- rescue Exception => e
51
- STDERR.puts 'Failed to retrieve initial gating info from API', e.backtrace
52
- end
53
-
54
- begin
55
- get_gating_info("#{CLOUDFRONT_GATING_INFO_ENDPOINT}/#{@env_key}", 'duration__cloudfront_gating_info') unless !@gating_info.nil?
56
- rescue Exception => e
57
- STDERR.puts 'Failed to retrieve initial gating info from CloudFront backup', e.backtrace
58
- end
59
-
60
- if EM.reactor_running? then
61
- em_subscribe_updates
62
- em_ingestion_timer
63
- else
64
- Thread.new {
65
- EM.run {
66
- em_subscribe_updates
67
- em_ingestion_timer
68
- }
69
- }
70
- end
71
- end
72
-
73
- def get_gating_info(url, stat_name)
74
- stat = FlaggerUtils::Stat.new(stat_name)
75
- stat.start
76
- conn = Faraday.new(url: url)
77
- response = conn.get do |req|
78
- req.options.timeout = @request_timeout
79
- end
80
- if response.status == 200
81
- @gating_info = FlaggerModels::GatingInfo.new(JSON.parse(response.body))
82
- update_sdk_vars(@gating_info)
83
- stat.stop
84
- save_stat(stat)
85
- else
86
- raise "Gating info request failed with HTTP code #{response.code}"
87
- end
88
- end
89
-
90
- def em_subscribe_updates()
91
- @ws_unsubbed = false
92
- @ws = Faye::WebSocket::Client.new("#{WEBSOCKET_GATING_INFO_ENDPOINT}?envkey=#{@env_key}")
93
-
94
- @ws.on :message do |event|
95
- begin
96
- @gating_info = FlaggerModels::GatingInfo.new(JSON.parse(event.data))
97
- update_sdk_vars(@gating_info)
98
- rescue Exception => e
99
- STDERR.puts 'Failed to update gating information', e
100
- end
101
- end
102
-
103
- @ws.on :close do |event|
104
- STDERR.puts ['Connection closed to Airship update server', event.code, event.reason] unless @ws_unsubbed
105
- EventMachine::Timer.new(5) do
106
- em_subscribe_updates
107
- end unless @ws_unsubbed
108
- end
109
- end
110
-
111
- def unsubscribe_updates()
112
- @ws_unsubbed = true
113
- @ws.close()
114
- end
115
-
116
- def update_sdk_vars(gating_info)
117
- if gating_info&.sdk_info&.ingestion_max_items then
118
- @ingestion_max_items = gating_info.sdk_info.ingestion_max_items
119
- end
120
- if gating_info&.sdk_info&.ingestion_interval then
121
- @ingestion_interval = gating_info.sdk_info.ingestion_interval
122
- end
123
- if gating_info&.sdk_info&.should_ingest_entities then
124
- @should_ingest_entities = gating_info.sdk_info.should_ingest_entities
125
- end
126
- if gating_info&.sdk_info&.should_ingest_stats then
127
- @should_ingest_stats = gating_info&.sdk_info&.should_ingest_stats
128
- end
129
- if gating_info&.sdk_info&.should_ingest_exposures then
130
- @should_ingest_exposures = gating_info&.sdk_info&.should_ingest_exposures
131
- end
132
- if gating_info&.sdk_info&.should_ingest_flags then
133
- @should_ingest_flags = gating_info&.sdk_info&.should_ingest_flags
134
- end
135
- end
136
-
137
- def maybe_ingest(should_ingest = false)
138
- if !@should_ingest_entities then
139
- @entities = []
140
- end
141
-
142
- if !@should_ingest_stats then
143
- @stats = []
144
- end
145
-
146
- if !@should_ingest_exposures then
147
- @exposures = []
148
- end
149
-
150
- if !@should_ingest_flags then
151
- @flags = Set[]
152
- end
153
-
154
- should_ingest |= (
155
- @entities.length > @ingestion_max_items ||
156
- @stats.length > @ingestion_max_items ||
157
- @exposures.length > @ingestion_max_items ||
158
- @flags.length > 0
159
- )
160
-
161
- if @first_ingestion then
162
- should_ingest |= @entities.length > 0
163
- @first_ingestion = !should_ingest
164
- end
165
-
166
- should_ingest &= (
167
- @entities.length > 0 ||
168
- @stats.length > 0 ||
169
- @exposures.length > 0 ||
170
- @flags.length > 0
171
- )
172
-
173
- if should_ingest then
174
- begin
175
- entities = @entities
176
- stats = @stats
177
- exposures = @exposures
178
- flags = @flags.to_a
179
- @entities = []
180
- @stats = []
181
- @exposures = []
182
- @ingested_flags.merge(@flags)
183
- @flags = Set[]
184
-
185
- conn = Faraday.new(url: "#{IDENTIFY_ENDPOINT}/#{@env_key}")
186
- response = conn.post do |req|
187
- req.options.timeout = @request_timeout
188
- req.headers['Content-Type'] = 'application/json'
189
- req.body = JSON.generate(
190
- {
191
- 'sdk_info' => {
192
- 'name' => 'ruby',
193
- 'version' => FlaggerUtils::VERSION
194
- },
195
- 'objects' => entities,
196
- 'stats' => stats,
197
- 'exposures' => exposures,
198
- 'flags' => flags
199
- })
200
- end
201
- if response.status != 200 then
202
- STDERR.puts 'Failed to upload entities', response.status
203
- nil
204
- end
205
- rescue Exception => e
206
- STDERR.puts 'Failed to upload entities', e
207
- nil
208
- end
209
- end
210
- end
211
-
212
- def em_ingestion_timer
213
- seconds = 0
214
- timer = EventMachine::PeriodicTimer.new(1) do
215
- timer.cancel unless !@cleaning
216
-
217
- seconds = (seconds + 1) % @ingestion_interval
218
- if seconds == 0 then
219
- EM.defer(proc {
220
- maybe_ingest(true)
221
- })
222
- end
223
- end
224
- end
225
-
226
- def cleanup()
227
- @cleaning = true
228
- unsubscribe_updates
229
- maybe_ingest(true)
230
- end
231
-
232
- def save_entity(entity)
233
- if @entity_lru_hashes[entity.id] != entity.hash then
234
- @entity_lru_hashes[entity.id] = entity.hash
235
- @entities.push(entity.attrs)
236
- maybe_ingest()
237
- end
238
- end
239
-
240
- def save_stat(stat)
241
- @stats.push(stat)
242
- @stats = FlaggerUtils::Stat.compact(@stats)
243
- maybe_ingest()
244
- end
245
-
246
- def save_exposure(exposure)
247
- @exposures.push(exposure)
248
- maybe_ingest()
249
- end
250
-
251
- def save_flag(flag)
252
- @flags.add(flag)
253
- maybe_ingest()
254
- end
255
-
256
- def stringify_keys(hash)
257
- if hash.is_a?(Hash) then
258
- hash.map {|k, v|
259
- [k.to_s, stringify_keys(v)]
260
- }.to_h
261
- else
262
- hash
263
- end
264
- end
265
-
266
- def identify_entity(entity_json)
267
- if !entity_json.is_a?(Hash) then
268
- STDERR.puts 'Entity must be a hash'
269
- nil
270
- else
271
- entity_json = stringify_keys(entity_json)
272
-
273
- entity = if entity_json['is_group'] then
274
- FlaggerModels::GroupEntity.new(entity_json)
275
- else
276
- FlaggerModels::SingleEntity.new(entity_json)
277
- end
278
-
279
- if !entity.valid? then
280
- STDERR.puts "Entity validation errors: #{entity.validation_errors}"
281
- nil
282
- else
283
- save_entity(entity)
284
-
285
- entity
286
- end
287
- end
288
- end
289
-
290
- def treatment(flagger_flag, entity_json)
291
- stat = FlaggerUtils::Stat.new('duration__get_treatment')
292
- stat.start()
293
-
294
- entity = identify_entity(entity_json)
295
- flag = @gating_info&.flag(flagger_flag.flag_name)
296
- if flag.nil? && !@ingested_flags.include?(flagger_flag.flag_name) then
297
- save_flag(flagger_flag.flag_name)
298
- end
299
- return_value = flag&.treatment(entity)
300
-
301
- if !return_value then
302
- return return_value
303
- end
304
-
305
- exposure = {
306
- flag: flagger_flag.flag_name,
307
- type: entity.type,
308
- id: entity.id,
309
- treatment: return_value[:treatment].codename,
310
- method_called: 'get_treatment',
311
- eligible: return_value[:eligible]
312
- }
313
-
314
- return_value = return_value[:treatment].codename
315
-
316
- save_exposure(exposure)
317
-
318
- stat.stop()
319
- save_stat(stat)
320
-
321
- return_value
322
- end
323
-
324
-
325
- def payload(flagger_flag, entity_json)
326
- stat = FlaggerUtils::Stat.new('duration__get_payload')
327
- stat.start()
328
-
329
- entity = identify_entity(entity_json)
330
- flag = @gating_info&.flag(flagger_flag.flag_name)
331
- if flag.nil? && !@ingested_flags.include?(flagger_flag.flag_name) then
332
- save_flag(flagger_flag.flag_name)
333
- end
334
- return_value = flag&.payload(entity)
335
-
336
- if !return_value then
337
- return return_value
338
- end
339
-
340
- exposure = {
341
- flag: flagger_flag.flag_name,
342
- type: entity.type,
343
- id: entity.id,
344
- treatment: return_value[:treatment].codename,
345
- method_called: 'get_payload',
346
- eligible: return_value[:eligible]
347
- }
348
-
349
- return_value = return_value[:treatment].payload
350
-
351
- save_exposure(exposure)
352
-
353
- stat.stop()
354
- save_stat(stat)
355
-
356
- return_value
357
- end
358
-
359
- def eligible?(flagger_flag, entity_json)
360
- stat = FlaggerUtils::Stat.new('duration__is_eligible')
361
- stat.start()
362
-
363
- entity = identify_entity(entity_json)
364
- flag = @gating_info&.flag(flagger_flag.flag_name)
365
- if flag.nil? && !@ingested_flags.include?(flagger_flag.flag_name) then
366
- save_flag(flagger_flag.flag_name)
367
- end
368
- return_value = flag&.eligible?(entity)
369
-
370
- if !return_value then
371
- return return_value
372
- end
373
-
374
- exposure = {
375
- flag: flagger_flag.flag_name,
376
- type: entity.type,
377
- id: entity.id,
378
- treatment: return_value[:treatment].codename,
379
- method_called: 'is_eligible',
380
- eligible: return_value[:eligible]
381
- }
382
-
383
- return_value = return_value[:eligible]
384
-
385
- save_exposure(exposure)
386
-
387
- stat.stop()
388
- save_stat(stat)
389
-
390
- return_value
391
- end
392
-
393
- def enabled?(flagger_flag, entity_json)
394
- stat = FlaggerUtils::Stat.new('duration__is_enabled')
395
- stat.start()
396
-
397
- entity = identify_entity(entity_json)
398
- flag = @gating_info&.flag(flagger_flag.flag_name)
399
- if flag.nil? && !@ingested_flags.include?(flagger_flag.flag_name) then
400
- save_flag(flagger_flag.flag_name)
401
- end
402
- return_value = flag&.enabled?(entity)
403
-
404
- if !return_value then
405
- return return_value
406
- end
407
-
408
- exposure = {
409
- flag: flagger_flag.flag_name,
410
- type: entity.type,
411
- id: entity.id,
412
- treatment: return_value[:treatment].codename,
413
- method_called: 'is_enabled',
414
- eligible: return_value[:eligible]
415
- }
416
-
417
- return_value = !return_value[:treatment].is_off_treatment
418
-
419
- save_exposure(exposure)
420
-
421
- stat.stop()
422
- save_stat(stat)
423
-
424
- return_value
425
- end
426
-
427
- def publish(entities)
428
- if !entities.is_a?(Array) then
429
- STDERR.puts 'The "publish" method takes an array of objects (aka entities).'
430
- return nil
431
- end
432
-
433
- entities.each do |entity|
434
- identify_entity(entity)
435
- end
436
-
437
- maybe_ingest(true)
438
- end
439
- end
440
- end
@@ -1,63 +0,0 @@
1
- require 'faraday'
2
- require 'json'
3
-
4
- module FlaggerEnvironments
5
- class MicroserviceDelegate
6
- attr_accessor :env_key
7
- attr_accessor :edge_url
8
- attr_accessor :request_timeout
9
-
10
- def initialize(env_key, edge_url, request_timeout)
11
- @env_key = env_key
12
- @edge_url = edge_url
13
- @request_timeout = request_timeout
14
- end
15
-
16
- def treatment(flag, entity)
17
- get_object_values(flag, entity)['treatment'] rescue nil
18
- end
19
-
20
- def payload(flag, entity)
21
- get_object_values(flag, entity)['payload'] rescue nil
22
- end
23
-
24
- def eligible?(flag, entity)
25
- get_object_values(flag, entity)['isEligible'] rescue nil
26
- end
27
-
28
- def enabled?(flag, entity)
29
- get_object_values(flag, entity)['isEnabled'] rescue nil
30
- end
31
-
32
- def publish(entities)
33
- end
34
-
35
- def cleanup()
36
- end
37
-
38
- private
39
- def get_object_values(flag, entity)
40
- begin
41
- conn = Faraday.new(url: "#{@edge_url}/v2/object-values/#{@env_key}")
42
- response = conn.post do |req|
43
- req.options.timeout = @request_timeout
44
- req.headers['Content-Type'] = 'application/json'
45
- req.body = JSON.generate({
46
- 'flag' => flag.flag_name,
47
- 'entity' => entity,
48
- })
49
- end
50
- if response.status == 200
51
- object_values = JSON.parse(response.body)
52
- object_values
53
- else
54
- puts 'Failed to connect to Airship edge server'
55
- nil
56
- end
57
- rescue Exception => e
58
- puts 'Failed to connect to Airship edge server', e
59
- nil
60
- end
61
- end
62
- end
63
- end