flipper 1.0.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +7 -3
- data/.github/workflows/examples.yml +27 -5
- data/Changelog.md +326 -272
- data/Gemfile +4 -4
- data/README.md +13 -11
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/cloud_setup.rb +16 -0
- data/examples/cloud/forked.rb +7 -2
- data/examples/cloud/threaded.rb +15 -18
- data/examples/expressions.rb +213 -0
- data/examples/strict.rb +18 -0
- data/flipper.gemspec +1 -2
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +10 -0
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +0 -4
- data/lib/flipper/adapters/http/client.rb +26 -7
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +18 -13
- data/lib/flipper/adapters/instrumented.rb +0 -4
- data/lib/flipper/adapters/memoizable.rb +14 -19
- data/lib/flipper/adapters/memory.rb +4 -6
- data/lib/flipper/adapters/operation_logger.rb +0 -4
- data/lib/flipper/adapters/poll.rb +1 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +4 -4
- data/lib/flipper/adapters/strict.rb +47 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/cloud/configuration.rb +121 -52
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
- data/lib/flipper/cloud/telemetry.rb +183 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +51 -0
- data/lib/flipper/engine.rb +27 -3
- data/lib/flipper/exporters/json/export.rb +1 -1
- data/lib/flipper/exporters/json/v1.rb +1 -1
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +11 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +9 -0
- data/lib/flipper/feature.rb +55 -0
- data/lib/flipper/gate.rb +1 -0
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/middleware/memoizer.rb +29 -13
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +1 -1
- data/lib/flipper/serializers/gzip.rb +24 -0
- data/lib/flipper/serializers/json.rb +19 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
- data/lib/flipper/test/shared_adapter_test.rb +24 -5
- data/lib/flipper/typecast.rb +34 -6
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +38 -1
- data/spec/flipper/adapter_builder_spec.rb +73 -0
- data/spec/flipper/adapter_spec.rb +1 -0
- data/spec/flipper/adapters/http_spec.rb +39 -5
- data/spec/flipper/adapters/memoizable_spec.rb +15 -15
- data/spec/flipper/adapters/read_only_spec.rb +26 -11
- data/spec/flipper/adapters/strict_spec.rb +62 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cloud/configuration_spec.rb +6 -23
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +156 -0
- data/spec/flipper/cloud_spec.rb +12 -12
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +39 -0
- data/spec/flipper/engine_spec.rb +108 -7
- data/spec/flipper/exporters/json/v1_spec.rb +3 -3
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/duration_spec.rb +43 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +13 -0
- data/spec/flipper/feature_spec.rb +360 -1
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
- data/spec/flipper/middleware/memoizer_spec.rb +67 -0
- data/spec/flipper/model/active_record_spec.rb +61 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +43 -7
- data/spec/flipper/types/actor_spec.rb +18 -1
- data/spec/flipper_integration_spec.rb +102 -4
- data/spec/flipper_spec.rb +89 -1
- data/spec/spec_helper.rb +5 -0
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +11 -3
- metadata +107 -18
- data/lib/flipper/cloud/instrumenter.rb +0 -48
@@ -1,3 +1,4 @@
|
|
1
|
+
require "logger"
|
1
2
|
require "socket"
|
2
3
|
require "flipper/adapters/http"
|
3
4
|
require "flipper/adapters/poll"
|
@@ -5,8 +6,9 @@ require "flipper/poller"
|
|
5
6
|
require "flipper/adapters/memory"
|
6
7
|
require "flipper/adapters/dual_write"
|
7
8
|
require "flipper/adapters/sync/synchronizer"
|
8
|
-
require "flipper/cloud/
|
9
|
-
require "
|
9
|
+
require "flipper/cloud/telemetry"
|
10
|
+
require "flipper/cloud/telemetry/instrumenter"
|
11
|
+
require "flipper/cloud/telemetry/submitter"
|
10
12
|
|
11
13
|
module Flipper
|
12
14
|
module Cloud
|
@@ -19,18 +21,13 @@ module Flipper
|
|
19
21
|
|
20
22
|
DEFAULT_URL = "https://www.flippercloud.io/adapter".freeze
|
21
23
|
|
22
|
-
# Private: Keeps track of brow instances so they can be shared across
|
23
|
-
# threads.
|
24
|
-
def self.brow_instances
|
25
|
-
@brow_instances ||= Concurrent::Map.new
|
26
|
-
end
|
27
|
-
|
28
24
|
# Public: The token corresponding to an environment on flippercloud.io.
|
29
25
|
attr_accessor :token
|
30
26
|
|
31
27
|
# Public: The url for http adapter. Really should only be customized for
|
32
|
-
|
33
|
-
|
28
|
+
# development work if you are me and you are not me. Feel free to
|
29
|
+
# forget you ever saw this.
|
30
|
+
attr_accessor :url
|
34
31
|
|
35
32
|
# Public: net/http read timeout for all http requests (default: 5).
|
36
33
|
attr_accessor :read_timeout
|
@@ -73,32 +70,25 @@ module Flipper
|
|
73
70
|
# occur or not.
|
74
71
|
attr_accessor :sync_secret
|
75
72
|
|
76
|
-
|
77
|
-
|
73
|
+
# Public: The logger to use for debugging inner workings.
|
74
|
+
attr_accessor :logger
|
78
75
|
|
79
|
-
|
80
|
-
|
81
|
-
end
|
76
|
+
# Public: Should the logger log or not (default: true).
|
77
|
+
attr_accessor :logging_enabled
|
82
78
|
|
83
|
-
|
84
|
-
|
85
|
-
@write_timeout = options.fetch(:write_timeout) { ENV.fetch("FLIPPER_CLOUD_WRITE_TIMEOUT", 5).to_f }
|
86
|
-
@sync_interval = options.fetch(:sync_interval) { ENV.fetch("FLIPPER_CLOUD_SYNC_INTERVAL", 10).to_f }
|
87
|
-
@sync_secret = options.fetch(:sync_secret) { ENV["FLIPPER_CLOUD_SYNC_SECRET"] }
|
88
|
-
@local_adapter = options.fetch(:local_adapter) { Adapters::Memory.new }
|
89
|
-
@debug_output = options[:debug_output]
|
90
|
-
@adapter_block = ->(adapter) { adapter }
|
91
|
-
self.url = options.fetch(:url) { ENV.fetch("FLIPPER_CLOUD_URL", DEFAULT_URL) }
|
79
|
+
# Public: The telemetry instance to use for tracking feature usage.
|
80
|
+
attr_accessor :telemetry
|
92
81
|
|
93
|
-
|
82
|
+
# Public: Should telemetry be enabled or not (default: false).
|
83
|
+
attr_accessor :telemetry_enabled
|
94
84
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
85
|
+
def initialize(options = {})
|
86
|
+
setup_auth options
|
87
|
+
setup_log options
|
88
|
+
setup_http options
|
89
|
+
setup_sync options
|
90
|
+
setup_adapter options
|
91
|
+
setup_telemetry options
|
102
92
|
end
|
103
93
|
|
104
94
|
# Public: Read or customize the http adapter. Calling without a block will
|
@@ -120,38 +110,31 @@ module Flipper
|
|
120
110
|
end
|
121
111
|
end
|
122
112
|
|
123
|
-
# Public:
|
124
|
-
attr_writer :url
|
125
|
-
|
113
|
+
# Public: Force a sync.
|
126
114
|
def sync
|
127
115
|
Flipper::Adapters::Sync::Synchronizer.new(local_adapter, http_adapter, {
|
128
116
|
instrumenter: instrumenter,
|
129
117
|
}).call
|
130
118
|
end
|
131
119
|
|
132
|
-
def brow
|
133
|
-
self.class.brow_instances.compute_if_absent(url + token) do
|
134
|
-
uri = URI.parse(url)
|
135
|
-
uri.path = "#{uri.path}/events".squeeze("/")
|
136
|
-
|
137
|
-
Brow::Client.new({
|
138
|
-
url: uri.to_s,
|
139
|
-
headers: {
|
140
|
-
"Accept" => "application/json",
|
141
|
-
"Content-Type" => "application/json",
|
142
|
-
"User-Agent" => "Flipper v#{VERSION} via Brow v#{Brow::VERSION}",
|
143
|
-
"Flipper-Cloud-Token" => @token,
|
144
|
-
}
|
145
|
-
})
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
120
|
# Public: The method that will be used to synchronize local adapter with
|
150
121
|
# cloud. (default: :poll, will be :webhook if sync_secret is set).
|
151
122
|
def sync_method
|
152
123
|
sync_secret ? :webhook : :poll
|
153
124
|
end
|
154
125
|
|
126
|
+
# Internal: The http client used by the http adapter. Exposed so we can
|
127
|
+
# use the same client for posting telemetry.
|
128
|
+
def http_client
|
129
|
+
http_adapter.client
|
130
|
+
end
|
131
|
+
|
132
|
+
# Internal: Logs message if logging is enabled.
|
133
|
+
def log(message, level: :debug)
|
134
|
+
return unless logging_enabled
|
135
|
+
logger.send(level, "name=flipper_cloud #{message}")
|
136
|
+
end
|
137
|
+
|
155
138
|
private
|
156
139
|
|
157
140
|
def app_adapter
|
@@ -184,6 +167,92 @@ module Flipper
|
|
184
167
|
},
|
185
168
|
})
|
186
169
|
end
|
170
|
+
|
171
|
+
def setup_auth(options)
|
172
|
+
set_option :token, options, required: true
|
173
|
+
end
|
174
|
+
|
175
|
+
def setup_log(options)
|
176
|
+
set_option :logging_enabled, options, default: true, typecast: :boolean
|
177
|
+
set_option :logger, options, from_env: false, default: -> {
|
178
|
+
if logging_enabled
|
179
|
+
Logger.new(STDOUT)
|
180
|
+
else
|
181
|
+
Logger.new("/dev/null")
|
182
|
+
end
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
def setup_http(options)
|
187
|
+
set_option :url, options, default: DEFAULT_URL
|
188
|
+
set_option :debug_output, options, from_env: false
|
189
|
+
set_option :read_timeout, options, default: 5, typecast: :float, minimum: 0.1
|
190
|
+
set_option :open_timeout, options, default: 2, typecast: :float, minimum: 0.1
|
191
|
+
set_option :write_timeout, options, default: 5, typecast: :float, minimum: 0.1
|
192
|
+
end
|
193
|
+
|
194
|
+
def setup_sync(options)
|
195
|
+
set_option :sync_interval, options, default: 10, typecast: :float, minimum: 10
|
196
|
+
set_option :sync_secret, options
|
197
|
+
end
|
198
|
+
|
199
|
+
def setup_adapter(options)
|
200
|
+
set_option :local_adapter, options, default: -> { Adapters::Memory.new }, from_env: false
|
201
|
+
@adapter_block = ->(adapter) { adapter }
|
202
|
+
end
|
203
|
+
|
204
|
+
def setup_telemetry(options)
|
205
|
+
# Needs to be after url and token assignments because they are used for
|
206
|
+
# uniqueness in Telemetry.instance_for.
|
207
|
+
set_option :telemetry, options, from_env: false, default: -> {
|
208
|
+
Telemetry.instance_for(self)
|
209
|
+
}
|
210
|
+
|
211
|
+
# This is alpha. Don't use this unless you are me. And you are not me.
|
212
|
+
set_option :telemetry_enabled, options, default: false, typecast: :boolean
|
213
|
+
instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
214
|
+
@instrumenter = if telemetry_enabled
|
215
|
+
Telemetry::Instrumenter.new(self, instrumenter)
|
216
|
+
else
|
217
|
+
instrumenter
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Internal: Super helper for defining an option that can be set via
|
222
|
+
# options hash or ENV with defaults, typecasting and minimums.
|
223
|
+
def set_option(name, options, default: nil, typecast: nil, minimum: nil, from_env: true, required: false)
|
224
|
+
env_var = "FLIPPER_CLOUD_#{name.to_s.upcase}"
|
225
|
+
value = options.fetch(name) {
|
226
|
+
default_value = default.respond_to?(:call) ? default.call : default
|
227
|
+
if from_env
|
228
|
+
ENV.fetch(env_var, default_value)
|
229
|
+
else
|
230
|
+
default_value
|
231
|
+
end
|
232
|
+
}
|
233
|
+
value = Flipper::Typecast.send("to_#{typecast}", value) if typecast
|
234
|
+
send("#{name}=", value)
|
235
|
+
enforce_minimum(name, minimum) if minimum
|
236
|
+
|
237
|
+
if required
|
238
|
+
option_value = send(name)
|
239
|
+
if option_value.nil? || option_value.empty?
|
240
|
+
message = "Flipper::Cloud #{name} is missing. Please "
|
241
|
+
message << "set #{env_var} or " if from_env
|
242
|
+
message << "provide #{name} (e.g. Flipper::Cloud.new(#{name}: value))."
|
243
|
+
raise ArgumentError, message
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Enforce minimum interval for tasks that run on a timer.
|
249
|
+
def enforce_minimum(name, minimum)
|
250
|
+
provided = send(name)
|
251
|
+
if provided && provided < minimum
|
252
|
+
warn "Flipper::Cloud##{name} must be at least #{minimum} seconds but was #{provided}. Using #{minimum} seconds."
|
253
|
+
send(:instance_variable_set, "@#{name}", minimum)
|
254
|
+
end
|
255
|
+
end
|
187
256
|
end
|
188
257
|
end
|
189
258
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Cloud
|
3
|
+
class Telemetry
|
4
|
+
class BackoffPolicy
|
5
|
+
# Private: The default minimum timeout between intervals in milliseconds.
|
6
|
+
MIN_TIMEOUT_MS = 1_000
|
7
|
+
|
8
|
+
# Private: The default maximum timeout between intervals in milliseconds.
|
9
|
+
MAX_TIMEOUT_MS = 30_000
|
10
|
+
|
11
|
+
# Private: The value to multiply the current interval with for each
|
12
|
+
# retry attempt.
|
13
|
+
MULTIPLIER = 1.5
|
14
|
+
|
15
|
+
# Private: The randomization factor to use to create a range around the
|
16
|
+
# retry interval.
|
17
|
+
RANDOMIZATION_FACTOR = 0.5
|
18
|
+
|
19
|
+
# Private
|
20
|
+
attr_reader :min_timeout_ms, :max_timeout_ms, :multiplier, :randomization_factor
|
21
|
+
|
22
|
+
# Private
|
23
|
+
attr_reader :attempts
|
24
|
+
|
25
|
+
# Public: Create new instance of backoff policy.
|
26
|
+
#
|
27
|
+
# options - The Hash of options.
|
28
|
+
# :min_timeout_ms - The minimum backoff timeout.
|
29
|
+
# :max_timeout_ms - The maximum backoff timeout.
|
30
|
+
# :multiplier - The value to multiply the current interval with for each
|
31
|
+
# retry attempt.
|
32
|
+
# :randomization_factor - The randomization factor to use to create a range
|
33
|
+
# around the retry interval.
|
34
|
+
def initialize(options = {})
|
35
|
+
@min_timeout_ms = options.fetch(:min_timeout_ms) {
|
36
|
+
ENV.fetch("FLIPPER_BACKOFF_MIN_TIMEOUT_MS", MIN_TIMEOUT_MS).to_i
|
37
|
+
}
|
38
|
+
@max_timeout_ms = options.fetch(:max_timeout_ms) {
|
39
|
+
ENV.fetch("FLIPPER_BACKOFF_MAX_TIMEOUT_MS", MAX_TIMEOUT_MS).to_i
|
40
|
+
}
|
41
|
+
@multiplier = options.fetch(:multiplier) {
|
42
|
+
ENV.fetch("FLIPPER_BACKOFF_MULTIPLIER", MULTIPLIER).to_f
|
43
|
+
}
|
44
|
+
@randomization_factor = options.fetch(:randomization_factor) {
|
45
|
+
ENV.fetch("FLIPPER_BACKOFF_RANDOMIZATION_FACTOR", RANDOMIZATION_FACTOR).to_f
|
46
|
+
}
|
47
|
+
|
48
|
+
unless @min_timeout_ms >= 0
|
49
|
+
raise ArgumentError, ":min_timeout_ms must be >= 0 but was #{@min_timeout_ms.inspect}"
|
50
|
+
end
|
51
|
+
|
52
|
+
unless @max_timeout_ms >= 0
|
53
|
+
raise ArgumentError, ":max_timeout_ms must be >= 0 but was #{@max_timeout_ms.inspect}"
|
54
|
+
end
|
55
|
+
|
56
|
+
unless @min_timeout_ms <= max_timeout_ms
|
57
|
+
raise ArgumentError, ":min_timeout_ms (#{@min_timeout_ms.inspect}) must be <= :max_timeout_ms (#{@max_timeout_ms.inspect})"
|
58
|
+
end
|
59
|
+
|
60
|
+
@attempts = 0
|
61
|
+
end
|
62
|
+
|
63
|
+
# Public: Returns the next backoff interval in milliseconds.
|
64
|
+
def next_interval
|
65
|
+
interval = @min_timeout_ms * (@multiplier**@attempts)
|
66
|
+
interval = add_jitter(interval, @randomization_factor)
|
67
|
+
|
68
|
+
@attempts += 1
|
69
|
+
|
70
|
+
[interval, @max_timeout_ms].min
|
71
|
+
end
|
72
|
+
|
73
|
+
def reset
|
74
|
+
@attempts = 0
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def add_jitter(base, randomization_factor)
|
80
|
+
random_number = rand
|
81
|
+
max_deviation = base * randomization_factor
|
82
|
+
deviation = random_number * max_deviation
|
83
|
+
|
84
|
+
if random_number < 0.5
|
85
|
+
base - deviation
|
86
|
+
else
|
87
|
+
base + deviation
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "delegate"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Cloud
|
5
|
+
class Telemetry
|
6
|
+
class Instrumenter < SimpleDelegator
|
7
|
+
def initialize(cloud_configuration, instrumenter)
|
8
|
+
super instrumenter
|
9
|
+
@cloud_configuration = cloud_configuration
|
10
|
+
end
|
11
|
+
|
12
|
+
def instrument(name, payload = {}, &block)
|
13
|
+
return_value = instrumenter.instrument(name, payload, &block)
|
14
|
+
@cloud_configuration.telemetry.record(name, payload)
|
15
|
+
return_value
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def instrumenter
|
21
|
+
__getobj__
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Cloud
|
3
|
+
class Telemetry
|
4
|
+
class Metric
|
5
|
+
attr_reader :key, :time, :result
|
6
|
+
|
7
|
+
def initialize(key, result, time = Time.now)
|
8
|
+
@key = key
|
9
|
+
@result = result
|
10
|
+
@time = time.to_i / 60 * 60
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_json(options = {})
|
14
|
+
data = {
|
15
|
+
"key" => key.to_s,
|
16
|
+
"time" => time,
|
17
|
+
"result" => result,
|
18
|
+
}
|
19
|
+
|
20
|
+
if options[:with]
|
21
|
+
data.merge!(options[:with])
|
22
|
+
end
|
23
|
+
|
24
|
+
data
|
25
|
+
end
|
26
|
+
|
27
|
+
def eql?(other)
|
28
|
+
self.class.eql?(other.class) &&
|
29
|
+
@key == other.key && @time == other.time && @result == other.result
|
30
|
+
end
|
31
|
+
alias :== :eql?
|
32
|
+
|
33
|
+
def hash
|
34
|
+
[self.class, @key, @time, @result].hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'concurrent/map'
|
2
|
+
require 'concurrent/atomic/atomic_fixnum'
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Cloud
|
6
|
+
class Telemetry
|
7
|
+
class MetricStorage
|
8
|
+
def initialize
|
9
|
+
@storage = Concurrent::Map.new { |h, k| h[k] = Concurrent::AtomicFixnum.new(0) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def increment(metric)
|
13
|
+
@storage[metric].increment
|
14
|
+
end
|
15
|
+
|
16
|
+
def drain
|
17
|
+
metrics = {}
|
18
|
+
@storage.keys.each do |metric|
|
19
|
+
metrics[metric] = @storage.delete(metric).value
|
20
|
+
end
|
21
|
+
metrics.freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty?
|
25
|
+
@storage.empty?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
require "flipper/typecast"
|
3
|
+
require "flipper/cloud/telemetry/backoff_policy"
|
4
|
+
|
5
|
+
module Flipper
|
6
|
+
module Cloud
|
7
|
+
class Telemetry
|
8
|
+
class Submitter
|
9
|
+
PATH = "/telemetry".freeze
|
10
|
+
SCHEMA_VERSION = "V1".freeze
|
11
|
+
GZIP_ENCODING = "gzip".freeze
|
12
|
+
|
13
|
+
Error = Class.new(StandardError) do
|
14
|
+
attr_reader :request_id, :response
|
15
|
+
|
16
|
+
def initialize(request_id, response)
|
17
|
+
@request_id = request_id
|
18
|
+
@response = response
|
19
|
+
super "Unexpected response code=#{response.code} request_id=#{request_id}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :cloud_configuration, :request_id, :backoff_policy
|
24
|
+
|
25
|
+
def initialize(cloud_configuration, backoff_policy: nil)
|
26
|
+
@cloud_configuration = cloud_configuration
|
27
|
+
@backoff_policy = backoff_policy || BackoffPolicy.new
|
28
|
+
@request_id = SecureRandom.uuid
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns Array of [response, error]. response and error could be nil
|
32
|
+
# but usually one or the other will be present.
|
33
|
+
def call(drained)
|
34
|
+
return if drained.empty?
|
35
|
+
body = to_body(drained)
|
36
|
+
return if body.nil? || body.empty?
|
37
|
+
retry_with_backoff(10) { submit(body) }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def to_body(drained)
|
43
|
+
enabled_metrics = drained.map { |metric, value|
|
44
|
+
metric.as_json(with: {"value" => value})
|
45
|
+
}
|
46
|
+
|
47
|
+
json = Typecast.to_json({
|
48
|
+
request_id: request_id,
|
49
|
+
enabled_metrics: enabled_metrics,
|
50
|
+
})
|
51
|
+
|
52
|
+
Typecast.to_gzip(json)
|
53
|
+
rescue => exception
|
54
|
+
@cloud_configuration.log "action=to_body request_id=#{request_id} error=#{exception.inspect}", level: :error
|
55
|
+
end
|
56
|
+
|
57
|
+
def retry_with_backoff(attempts, &block)
|
58
|
+
result, caught_exception = nil
|
59
|
+
should_retry = false
|
60
|
+
attempts_remaining = attempts - 1
|
61
|
+
|
62
|
+
begin
|
63
|
+
result, should_retry = yield
|
64
|
+
return [result, nil] unless should_retry
|
65
|
+
rescue => error
|
66
|
+
@cloud_configuration.log "action=post_to_cloud attempts_remaining=#{attempts_remaining} error=#{error.inspect}", level: :error
|
67
|
+
should_retry = true
|
68
|
+
caught_exception = error
|
69
|
+
end
|
70
|
+
|
71
|
+
if should_retry && attempts_remaining > 0
|
72
|
+
sleep @backoff_policy.next_interval.to_f / 1000
|
73
|
+
retry_with_backoff attempts_remaining, &block
|
74
|
+
else
|
75
|
+
[result, caught_exception]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def submit(body)
|
80
|
+
client = @cloud_configuration.http_client
|
81
|
+
client.add_header :schema_version, SCHEMA_VERSION
|
82
|
+
client.add_header :content_encoding, GZIP_ENCODING
|
83
|
+
|
84
|
+
response = client.post PATH, body
|
85
|
+
code = response.code.to_i
|
86
|
+
|
87
|
+
# Raise error and retry for retriable status codes.
|
88
|
+
# FIXME: what about redirects?
|
89
|
+
if code < 200 || code == 429 || code >= 500
|
90
|
+
raise Error.new(request_id, response)
|
91
|
+
end
|
92
|
+
|
93
|
+
response
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|