prefab-cloud-ruby 0.13.1 → 0.13.2
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/workflows/ruby.yml +39 -0
- data/.tool-versions +1 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +11 -4
- data/Gemfile.lock +52 -32
- data/README.md +19 -1
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/compile_protos.sh +3 -0
- data/lib/prefab/auth_interceptor.rb +1 -0
- data/lib/prefab/cancellable_interceptor.rb +1 -0
- data/lib/prefab/client.rb +51 -38
- data/lib/prefab/config_client.rb +145 -73
- data/lib/prefab/config_helper.rb +29 -0
- data/lib/prefab/config_loader.rb +98 -13
- data/lib/prefab/config_resolver.rb +56 -49
- data/lib/prefab/error.rb +6 -0
- data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
- data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
- data/lib/prefab/errors/missing_default_error.rb +13 -0
- data/lib/prefab/feature_flag_client.rb +129 -11
- data/lib/prefab/internal_logger.rb +29 -0
- data/lib/prefab/logger_client.rb +10 -8
- data/lib/prefab/murmer3.rb +1 -0
- data/lib/prefab/noop_cache.rb +1 -0
- data/lib/prefab/noop_stats.rb +1 -0
- data/lib/prefab/options.rb +82 -0
- data/lib/prefab/ratelimit_client.rb +1 -0
- data/lib/prefab-cloud-ruby.rb +10 -0
- data/lib/prefab_pb.rb +214 -132
- data/lib/prefab_services_pb.rb +35 -6
- data/prefab-cloud-ruby.gemspec +29 -10
- data/run_test_harness_server.sh +8 -0
- data/test/.prefab.test.config.yaml +27 -1
- data/test/harness_server.rb +64 -0
- data/test/test_client.rb +98 -0
- data/test/test_config_client.rb +56 -0
- data/test/test_config_loader.rb +39 -25
- data/test/test_config_resolver.rb +134 -38
- data/test/test_feature_flag_client.rb +277 -35
- data/test/test_helper.rb +70 -4
- data/test/test_logger.rb +23 -29
- metadata +69 -14
- data/.ruby-version +0 -1
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
module Errors
|
5
|
+
class InvalidApiKeyError < Prefab::Error
|
6
|
+
def initialize(key)
|
7
|
+
if key.nil? || key.empty?
|
8
|
+
message = "No API key. Set PREFAB_API_KEY env var or use PREFAB_DATASOURCES=LOCAL_ONLY"
|
9
|
+
|
10
|
+
super(message)
|
11
|
+
else
|
12
|
+
message = "Your API key format is invalid. Expecting something like 123-development-yourapikey-SDK. You provided `#{key}`"
|
13
|
+
|
14
|
+
super(message)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
module Errors
|
5
|
+
class MissingDefaultError < Prefab::Error
|
6
|
+
def initialize(key)
|
7
|
+
message = "No value found for key '#{key}' and no default was provided.\n\nIf you'd prefer returning `nil` rather than raising when this occurs, modify the `on_no_default` value you provide in your Prefab::Options."
|
8
|
+
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Prefab
|
2
3
|
class FeatureFlagClient
|
4
|
+
include Prefab::ConfigHelper
|
3
5
|
MAX_32_FLOAT = 4294967294.0
|
4
6
|
|
5
7
|
def initialize(base_client)
|
@@ -14,38 +16,154 @@ module Prefab
|
|
14
16
|
feature_is_on_for?(feature_name, nil)
|
15
17
|
end
|
16
18
|
|
17
|
-
def feature_is_on_for?(feature_name, lookup_key, attributes:
|
19
|
+
def feature_is_on_for?(feature_name, lookup_key, attributes: {})
|
18
20
|
@base_client.stats.increment("prefab.featureflag.on", tags: ["feature:#{feature_name}"])
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
+
return is_on?(_get(feature_name, lookup_key, attributes, default: false))
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(feature_name, lookup_key=nil, attributes={}, default: false)
|
26
|
+
variant = _get(feature_name, lookup_key, attributes, default: default)
|
27
|
+
|
28
|
+
value_of_variant_or_nil(variant, default)
|
22
29
|
end
|
23
30
|
|
24
31
|
private
|
25
32
|
|
26
|
-
def
|
27
|
-
if
|
33
|
+
def value_of_variant_or_nil(variant_maybe, default)
|
34
|
+
if variant_maybe.nil?
|
35
|
+
default != Prefab::Client::NO_DEFAULT_PROVIDED ? default : nil
|
36
|
+
else
|
37
|
+
value_of_variant(variant_maybe)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def _get(feature_name, lookup_key=nil, attributes={}, default:)
|
42
|
+
feature_obj = @base_client.config_client.get(feature_name, default)
|
43
|
+
config_obj = @base_client.config_client.get_config_obj(feature_name)
|
44
|
+
|
45
|
+
return nil if feature_obj.nil? || config_obj.nil?
|
46
|
+
|
47
|
+
if feature_obj == !!feature_obj
|
48
|
+
return feature_obj
|
49
|
+
end
|
50
|
+
|
51
|
+
variants = config_obj.variants
|
52
|
+
get_variant(feature_name, lookup_key, attributes, feature_obj, variants)
|
53
|
+
end
|
54
|
+
|
55
|
+
def is_on?(variant)
|
56
|
+
if variant.nil?
|
28
57
|
return false
|
29
58
|
end
|
30
59
|
|
31
|
-
|
32
|
-
|
33
|
-
|
60
|
+
if variant == !!variant
|
61
|
+
return variant
|
62
|
+
end
|
63
|
+
|
64
|
+
variant.bool
|
65
|
+
rescue
|
66
|
+
@base_client.log.info("is_on? methods only work for boolean feature flags variants. This feature flags variant is '#{variant}'. Returning false")
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_variant(feature_name, lookup_key, attributes, feature_obj, variants)
|
71
|
+
if !feature_obj.active
|
72
|
+
return get_variant_obj(variants, feature_obj.inactive_variant_idx)
|
34
73
|
end
|
35
74
|
|
75
|
+
#default to inactive
|
76
|
+
variant_weights = [Prefab::VariantWeight.new(variant_idx: feature_obj.inactive_variant_idx, weight: 1)]
|
77
|
+
|
78
|
+
# if rules.match
|
79
|
+
feature_obj.rules.each do |rule|
|
80
|
+
if criteria_match?(rule.criteria, lookup_key, attributes)
|
81
|
+
variant_weights = rule.variant_weights
|
82
|
+
break
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
percent_through_distribution = rand()
|
36
87
|
if lookup_key
|
37
|
-
|
88
|
+
percent_through_distribution = get_user_pct(feature_name, lookup_key)
|
38
89
|
end
|
39
90
|
|
40
|
-
|
91
|
+
variant_idx = get_variant_idx_from_weights(variant_weights, percent_through_distribution, feature_name)
|
92
|
+
|
93
|
+
return get_variant_obj(variants, variant_idx)
|
94
|
+
end
|
95
|
+
|
96
|
+
def get_variant_obj(variants, idx)
|
97
|
+
# our array is 0 based, but the idx are 1 based so the protos are clearly set
|
98
|
+
return variants[idx - 1] if variants.length >= idx
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_variant_idx_from_weights(variant_weights, percent_through_distribution, feature_name)
|
103
|
+
distrubution_space = variant_weights.inject(0) { |sum, v| sum + v.weight }
|
104
|
+
bucket = distrubution_space * percent_through_distribution
|
105
|
+
sum = 0
|
106
|
+
variant_weights.each do |variant_weight|
|
107
|
+
if bucket < sum + variant_weight.weight
|
108
|
+
return variant_weight.variant_idx
|
109
|
+
else
|
110
|
+
sum += variant_weight.weight
|
111
|
+
end
|
112
|
+
end
|
113
|
+
# variants didn't add up to 100%
|
114
|
+
@base_client.log.info("Variants of #{feature_name} did not add to 100%")
|
115
|
+
return variant_weights.last.variant_idx
|
41
116
|
end
|
42
117
|
|
43
118
|
def get_user_pct(feature, lookup_key)
|
44
|
-
to_hash = "#{@base_client.
|
119
|
+
to_hash = "#{@base_client.project_id}#{feature}#{lookup_key}"
|
45
120
|
int_value = Murmur3.murmur3_32(to_hash)
|
46
121
|
int_value / MAX_32_FLOAT
|
47
122
|
end
|
48
123
|
|
124
|
+
# def criteria_match?(rule, lookup_key, attributes)
|
125
|
+
#
|
126
|
+
# end
|
127
|
+
def criteria_match?(criteria, lookup_key, attributes)
|
128
|
+
if criteria.operator == :ALWAYS_TRUE
|
129
|
+
return true
|
130
|
+
elsif criteria.operator == :LOOKUP_KEY_IN
|
131
|
+
return criteria.values.include?(lookup_key)
|
132
|
+
elsif criteria.operator == :LOOKUP_KEY_NOT_IN
|
133
|
+
return !criteria.values.include?(lookup_key)
|
134
|
+
elsif criteria.operator == :IN_SEG
|
135
|
+
return segment_matches(criteria.values, lookup_key, attributes).any?
|
136
|
+
elsif criteria.operator == :NOT_IN_SEG
|
137
|
+
return segment_matches(criteria.values, lookup_key, attributes).none?
|
138
|
+
elsif criteria.operator == :PROP_IS_ONE_OF
|
139
|
+
return criteria.values.include?(attributes[criteria.property]) || criteria.values.include?(attributes[criteria.property.to_sym])
|
140
|
+
elsif criteria.operator == :PROP_IS_NOT_ONE_OF
|
141
|
+
return !(criteria.values.include?(attributes[criteria.property]) || criteria.values.include?(attributes[criteria.property.to_sym]))
|
142
|
+
end
|
143
|
+
@base_client.log.info("Unknown Operator")
|
144
|
+
false
|
145
|
+
end
|
146
|
+
|
147
|
+
# evaluate each segment key and return whether each one matches
|
148
|
+
# there should be an associated segment available as a standard config obj
|
149
|
+
def segment_matches(segment_keys, lookup_key, attributes)
|
150
|
+
segment_keys.map do |segment_key|
|
151
|
+
segment = @base_client.config_client.get(segment_key)
|
152
|
+
if segment.nil?
|
153
|
+
@base_client.log.info("Missing Segment")
|
154
|
+
false
|
155
|
+
else
|
156
|
+
segment_match?(segment, lookup_key, attributes)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# does a given segment match?
|
162
|
+
def segment_match?(segment, lookup_key, attributes)
|
163
|
+
segment.criterion.map do |criteria|
|
164
|
+
criteria_match?(criteria, lookup_key, attributes)
|
165
|
+
end.any?
|
166
|
+
end
|
49
167
|
end
|
50
168
|
end
|
51
169
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Prefab
|
3
|
+
class InternalLogger < Logger
|
4
|
+
def initialize(path, logger)
|
5
|
+
@path = path
|
6
|
+
@logger = logger
|
7
|
+
end
|
8
|
+
|
9
|
+
def debug(progname = nil, &block)
|
10
|
+
@logger.log_internal yield, @path, progname, DEBUG
|
11
|
+
end
|
12
|
+
|
13
|
+
def info(progname = nil, &block)
|
14
|
+
@logger.log_internal yield, @path, progname, INFO
|
15
|
+
end
|
16
|
+
|
17
|
+
def warn(progname = nil, &block)
|
18
|
+
@logger.log_internal yield, @path, progname, WARN
|
19
|
+
end
|
20
|
+
|
21
|
+
def error(progname = nil, &block)
|
22
|
+
@logger.log_internal yield, @path, progname, ERROR
|
23
|
+
end
|
24
|
+
|
25
|
+
def fatal(progname = nil, &block)
|
26
|
+
@logger.log_internal yield, @path, progname, FATAL
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/prefab/logger_client.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Prefab
|
2
3
|
class LoggerClient < Logger
|
3
4
|
|
4
|
-
SEP = "."
|
5
|
-
BASE = "log_level"
|
5
|
+
SEP = "."
|
6
|
+
BASE = "log_level"
|
7
|
+
UNKNOWN = "unknown"
|
6
8
|
|
7
9
|
def initialize(logdev, formatter: nil)
|
8
10
|
super(logdev)
|
@@ -107,10 +109,10 @@ module Prefab
|
|
107
109
|
|
108
110
|
# Find the closest match to 'log_level.path' in config
|
109
111
|
def level_of(path)
|
110
|
-
closest_log_level_match = @config_client.get(BASE
|
112
|
+
closest_log_level_match = @config_client.get(BASE, :warn)
|
111
113
|
path.split(SEP).inject([BASE]) do |memo, n|
|
112
114
|
memo << n
|
113
|
-
val = @config_client.get(memo.join(SEP))
|
115
|
+
val = @config_client.get(memo.join(SEP), nil)
|
114
116
|
unless val.nil?
|
115
117
|
closest_log_level_match = val
|
116
118
|
end
|
@@ -122,12 +124,12 @@ module Prefab
|
|
122
124
|
# sanitize & clean the path of the caller so the key
|
123
125
|
# looks like log_level.app.models.user
|
124
126
|
def get_path(absolute_path, base_label)
|
125
|
-
path = absolute_path
|
127
|
+
path = (absolute_path || UNKNOWN).dup
|
126
128
|
path.slice! Dir.pwd
|
127
129
|
|
128
130
|
path.gsub!(/.*?(?=\/lib\/)/im, "")
|
129
131
|
|
130
|
-
path =
|
132
|
+
path = path.gsub("/", SEP).gsub(".rb", "") + SEP + base_label
|
131
133
|
path.slice! ".lib"
|
132
134
|
path.slice! SEP
|
133
135
|
path
|
@@ -141,8 +143,8 @@ module Prefab
|
|
141
143
|
# StubConfigClient to be used while config client initializes
|
142
144
|
# since it may log
|
143
145
|
class BootstrappingConfigClient
|
144
|
-
def get(key)
|
145
|
-
ENV["PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL"] ||
|
146
|
+
def get(key, default=nil)
|
147
|
+
ENV["PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL"] || default
|
146
148
|
end
|
147
149
|
end
|
148
150
|
end
|
data/lib/prefab/murmer3.rb
CHANGED
data/lib/prefab/noop_cache.rb
CHANGED
data/lib/prefab/noop_stats.rb
CHANGED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Prefab
|
3
|
+
class Options
|
4
|
+
attr_reader :api_key
|
5
|
+
attr_reader :logdev
|
6
|
+
attr_reader :log_formatter
|
7
|
+
attr_reader :stats
|
8
|
+
attr_reader :shared_cache
|
9
|
+
attr_reader :namespace
|
10
|
+
attr_reader :prefab_api_url
|
11
|
+
attr_reader :prefab_grpc_url
|
12
|
+
attr_reader :on_no_default
|
13
|
+
attr_reader :initialization_timeout_sec
|
14
|
+
attr_reader :on_init_failure
|
15
|
+
attr_reader :prefab_config_override_dir
|
16
|
+
attr_reader :prefab_config_classpath_dir
|
17
|
+
attr_reader :defaults_env
|
18
|
+
|
19
|
+
DEFAULT_LOG_FORMATTER = proc { |severity, datetime, progname, msg|
|
20
|
+
"#{severity.ljust(5)} #{datetime}: #{progname} #{msg}\n"
|
21
|
+
}
|
22
|
+
|
23
|
+
module ON_INITIALIZATION_FAILURE
|
24
|
+
RAISE = 1
|
25
|
+
RETURN = 2
|
26
|
+
end
|
27
|
+
module ON_NO_DEFAULT
|
28
|
+
RAISE = 1
|
29
|
+
RETURN_NIL = 2
|
30
|
+
end
|
31
|
+
module DATASOURCES
|
32
|
+
ALL = 1
|
33
|
+
LOCAL_ONLY = 2
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(
|
37
|
+
api_key: ENV['PREFAB_API_KEY'],
|
38
|
+
logdev: $stdout,
|
39
|
+
stats: NoopStats.new, # receives increment("prefab.limitcheck", {:tags=>["policy_group:page_view", "pass:true"]})
|
40
|
+
shared_cache: NoopCache.new, # Something that quacks like Rails.cache ideally memcached
|
41
|
+
namespace: "",
|
42
|
+
log_formatter: DEFAULT_LOG_FORMATTER,
|
43
|
+
prefab_api_url: ENV["PREFAB_API_URL"] || 'https://api.prefab.cloud',
|
44
|
+
prefab_grpc_url: ENV["PREFAB_GRPC_URL"] || 'grpc.prefab.cloud:443',
|
45
|
+
on_no_default: ON_NO_DEFAULT::RAISE, # options :raise, :warn_and_return_nil,
|
46
|
+
initialization_timeout_sec: 10, # how long to wait before on_init_failure
|
47
|
+
on_init_failure: ON_INITIALIZATION_FAILURE::RAISE, #options :unlock_and_continue, :lock_and_keep_trying, :raise
|
48
|
+
# new_config_callback: nil, #callback method
|
49
|
+
# live_override_url: nil,
|
50
|
+
prefab_datasources: ENV['PREFAB_DATASOURCES'] == "LOCAL_ONLY" ? DATASOURCES::LOCAL_ONLY : DATASOURCES::ALL,
|
51
|
+
prefab_config_override_dir: Dir.home,
|
52
|
+
prefab_config_classpath_dir: ".",
|
53
|
+
defaults_env: ""
|
54
|
+
)
|
55
|
+
# debugger
|
56
|
+
@api_key = api_key
|
57
|
+
@logdev = logdev
|
58
|
+
@stats = stats
|
59
|
+
@shared_cache = shared_cache
|
60
|
+
@namespace = namespace
|
61
|
+
@log_formatter = log_formatter
|
62
|
+
@prefab_api_url = prefab_api_url
|
63
|
+
@prefab_grpc_url = prefab_grpc_url
|
64
|
+
@on_no_default = on_no_default
|
65
|
+
@initialization_timeout_sec = initialization_timeout_sec
|
66
|
+
@on_init_failure = on_init_failure
|
67
|
+
@prefab_datasources = prefab_datasources
|
68
|
+
@prefab_config_classpath_dir = prefab_config_classpath_dir
|
69
|
+
@prefab_config_override_dir = prefab_config_override_dir
|
70
|
+
@defaults_env = defaults_env
|
71
|
+
end
|
72
|
+
|
73
|
+
def local_only?
|
74
|
+
@prefab_datasources == DATASOURCES::LOCAL_ONLY
|
75
|
+
end
|
76
|
+
|
77
|
+
# https://api.prefab.cloud -> https://api-prefab-cloud.global.ssl.fastly.net
|
78
|
+
def url_for_api_cdn
|
79
|
+
ENV['PREFAB_CDN_URL'] || "#{@prefab_api_url.gsub(/\./, "-")}.global.ssl.fastly.net"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/prefab-cloud-ruby.rb
CHANGED
@@ -1,9 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "concurrent/atomics"
|
2
3
|
require 'concurrent'
|
3
4
|
require 'faraday'
|
4
5
|
require 'openssl'
|
6
|
+
require 'openssl'
|
7
|
+
require 'ld-eventsource'
|
5
8
|
require 'prefab_pb'
|
9
|
+
require 'prefab/error'
|
10
|
+
require 'prefab/errors/initialization_timeout_error'
|
11
|
+
require 'prefab/errors/invalid_api_key_error'
|
12
|
+
require 'prefab/errors/missing_default_error'
|
6
13
|
require 'prefab_services_pb'
|
14
|
+
require 'prefab/options'
|
15
|
+
require 'prefab/internal_logger'
|
16
|
+
require 'prefab/config_helper'
|
7
17
|
require 'prefab/config_loader'
|
8
18
|
require 'prefab/config_resolver'
|
9
19
|
require 'prefab/client'
|