flipper 1.0.0 → 1.1.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.
- 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 +42 -0
- 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 +28 -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/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/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 +104 -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
|