flagger 2.0.5 → 3.0.1

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.
@@ -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