prefab-cloud-ruby 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 +7 -0
- data/.envrc.sample +3 -0
- data/.github/workflows/ruby.yml +46 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +169 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +188 -0
- data/LICENSE.txt +20 -0
- data/README.md +94 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/bin/console +21 -0
- data/compile_protos.sh +18 -0
- data/lib/prefab/client.rb +153 -0
- data/lib/prefab/config_client.rb +292 -0
- data/lib/prefab/config_client_presenter.rb +18 -0
- data/lib/prefab/config_loader.rb +84 -0
- data/lib/prefab/config_resolver.rb +77 -0
- data/lib/prefab/config_value_unwrapper.rb +115 -0
- data/lib/prefab/config_value_wrapper.rb +18 -0
- data/lib/prefab/context.rb +179 -0
- data/lib/prefab/context_shape.rb +20 -0
- data/lib/prefab/context_shape_aggregator.rb +65 -0
- data/lib/prefab/criteria_evaluator.rb +136 -0
- data/lib/prefab/encryption.rb +65 -0
- data/lib/prefab/error.rb +6 -0
- data/lib/prefab/errors/env_var_parse_error.rb +11 -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/errors/missing_env_var_error.rb +11 -0
- data/lib/prefab/errors/uninitialized_error.rb +13 -0
- data/lib/prefab/evaluation.rb +52 -0
- data/lib/prefab/evaluation_summary_aggregator.rb +87 -0
- data/lib/prefab/example_contexts_aggregator.rb +78 -0
- data/lib/prefab/exponential_backoff.rb +21 -0
- data/lib/prefab/feature_flag_client.rb +42 -0
- data/lib/prefab/http_connection.rb +41 -0
- data/lib/prefab/internal_logger.rb +16 -0
- data/lib/prefab/local_config_parser.rb +151 -0
- data/lib/prefab/log_path_aggregator.rb +69 -0
- data/lib/prefab/logger_client.rb +264 -0
- data/lib/prefab/murmer3.rb +50 -0
- data/lib/prefab/options.rb +208 -0
- data/lib/prefab/periodic_sync.rb +69 -0
- data/lib/prefab/prefab.rb +56 -0
- data/lib/prefab/rate_limit_cache.rb +41 -0
- data/lib/prefab/resolved_config_presenter.rb +86 -0
- data/lib/prefab/time_helpers.rb +7 -0
- data/lib/prefab/weighted_value_resolver.rb +42 -0
- data/lib/prefab/yaml_config_parser.rb +34 -0
- data/lib/prefab-cloud-ruby.rb +57 -0
- data/lib/prefab_pb.rb +93 -0
- data/prefab-cloud-ruby.gemspec +155 -0
- data/test/.prefab.default.config.yaml +2 -0
- data/test/.prefab.unit_tests.config.yaml +28 -0
- data/test/integration_test.rb +150 -0
- data/test/integration_test_helpers.rb +151 -0
- data/test/support/common_helpers.rb +180 -0
- data/test/support/mock_base_client.rb +42 -0
- data/test/support/mock_config_client.rb +19 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_client.rb +444 -0
- data/test/test_config_client.rb +109 -0
- data/test/test_config_loader.rb +117 -0
- data/test/test_config_resolver.rb +430 -0
- data/test/test_config_value_unwrapper.rb +224 -0
- data/test/test_config_value_wrapper.rb +42 -0
- data/test/test_context.rb +203 -0
- data/test/test_context_shape.rb +50 -0
- data/test/test_context_shape_aggregator.rb +147 -0
- data/test/test_criteria_evaluator.rb +726 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluation_summary_aggregator.rb +162 -0
- data/test/test_example_contexts_aggregator.rb +238 -0
- data/test/test_exponential_backoff.rb +18 -0
- data/test/test_feature_flag_client.rb +48 -0
- data/test/test_helper.rb +17 -0
- data/test/test_integration.rb +58 -0
- data/test/test_local_config_parser.rb +147 -0
- data/test/test_log_path_aggregator.rb +62 -0
- data/test/test_logger.rb +621 -0
- data/test/test_logger_initialization.rb +12 -0
- data/test/test_options.rb +75 -0
- data/test/test_prefab.rb +12 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_weighted_value_resolver.rb +71 -0
- metadata +337 -0
@@ -0,0 +1,264 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class LoggerClient < ::Logger
|
5
|
+
SEP = '.'
|
6
|
+
BASE_KEY = 'log-level'
|
7
|
+
UNKNOWN_PATH = 'unknown.'
|
8
|
+
LOG_TAGS = 'log.tags'
|
9
|
+
REQ_TAGS = 'req.tags'
|
10
|
+
|
11
|
+
LOG_LEVEL_LOOKUPS = {
|
12
|
+
PrefabProto::LogLevel::NOT_SET_LOG_LEVEL => ::Logger::DEBUG,
|
13
|
+
PrefabProto::LogLevel::TRACE => ::Logger::DEBUG,
|
14
|
+
PrefabProto::LogLevel::DEBUG => ::Logger::DEBUG,
|
15
|
+
PrefabProto::LogLevel::INFO => ::Logger::INFO,
|
16
|
+
PrefabProto::LogLevel::WARN => ::Logger::WARN,
|
17
|
+
PrefabProto::LogLevel::ERROR => ::Logger::ERROR,
|
18
|
+
PrefabProto::LogLevel::FATAL => ::Logger::FATAL
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def self.instance
|
22
|
+
@@shared_instance ||= LoggerClient.new($stdout)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(logdev, log_path_aggregator: nil, formatter: Options::DEFAULT_LOG_FORMATTER, prefix: nil)
|
26
|
+
super(logdev)
|
27
|
+
self.formatter = Prefab::Logging::FormatterBase.new(formatter_proc: formatter, logger_client: self)
|
28
|
+
@config_client = BootstrappingConfigClient.new
|
29
|
+
@silences = Concurrent::Map.new(initial_capacity: 2)
|
30
|
+
@recurse_check = Concurrent::Map.new(initial_capacity: 2)
|
31
|
+
@prefix = "#{prefix}#{prefix && '.'}"
|
32
|
+
|
33
|
+
@context_keys_map = Concurrent::Map.new(initial_capacity: 4)
|
34
|
+
|
35
|
+
@log_path_aggregator = log_path_aggregator
|
36
|
+
@@shared_instance = self
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_context_keys(*keys)
|
40
|
+
context_keys.merge(keys)
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_context_keys(*keys)
|
44
|
+
context_keys.merge(keys)
|
45
|
+
yield
|
46
|
+
ensure
|
47
|
+
context_keys.subtract(keys)
|
48
|
+
end
|
49
|
+
|
50
|
+
def internal_logger(path = nil)
|
51
|
+
InternalLogger.new(path, self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def context_keys
|
55
|
+
@context_keys_map.fetch_or_store(local_log_id, Concurrent::Set.new)
|
56
|
+
end
|
57
|
+
|
58
|
+
# InternalLoggers Will Call This
|
59
|
+
def add_internal(severity, message, progname, loc, log_context = {}, &block)
|
60
|
+
path_loc = get_loc_path(loc)
|
61
|
+
path = @prefix + path_loc
|
62
|
+
|
63
|
+
log(message, path, progname, severity, log_context, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def log_internal(severity, message, path, log_context = {}, &block)
|
67
|
+
return if @recurse_check[local_log_id]
|
68
|
+
@recurse_check[local_log_id] = true
|
69
|
+
begin
|
70
|
+
log(message, path, nil, severity, log_context, &block)
|
71
|
+
ensure
|
72
|
+
@recurse_check[local_log_id] = false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def log(message, path, progname, severity, log_context = {})
|
77
|
+
severity ||= ::Logger::UNKNOWN
|
78
|
+
|
79
|
+
return true if !should_log? severity, path
|
80
|
+
return true if @logdev.nil? || @silences[local_log_id]
|
81
|
+
|
82
|
+
progname = @progname if progname.nil?
|
83
|
+
|
84
|
+
if message.nil?
|
85
|
+
if block_given?
|
86
|
+
message = yield
|
87
|
+
else
|
88
|
+
message = progname
|
89
|
+
progname = @progname
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@logdev.write(
|
94
|
+
format_message(format_severity(severity), Time.now, progname, message, path, stringify_keys(log_context.merge(fetch_context_for_context_keys)))
|
95
|
+
)
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def should_log?(severity, path)
|
100
|
+
@log_path_aggregator&.push(path, severity)
|
101
|
+
severity >= level_of(path)
|
102
|
+
end
|
103
|
+
|
104
|
+
def debug(progname = nil, **log_context, &block)
|
105
|
+
add_internal(DEBUG, nil, progname, caller_locations(1, 1)[0], log_context, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def info(progname = nil, **log_context, &block)
|
109
|
+
add_internal(INFO, nil, progname, caller_locations(1, 1)[0], log_context, &block)
|
110
|
+
end
|
111
|
+
|
112
|
+
def warn(progname = nil, **log_context, &block)
|
113
|
+
add_internal(WARN, nil, progname, caller_locations(1, 1)[0], log_context, &block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def error(progname = nil, **log_context, &block)
|
117
|
+
add_internal(ERROR, nil, progname, caller_locations(1, 1)[0], log_context, &block)
|
118
|
+
end
|
119
|
+
|
120
|
+
def fatal(progname = nil, **log_context, &block)
|
121
|
+
add_internal(FATAL, nil, progname, caller_locations(1, 1)[0], log_context, &block)
|
122
|
+
end
|
123
|
+
|
124
|
+
def debug?
|
125
|
+
true
|
126
|
+
end
|
127
|
+
|
128
|
+
def info?
|
129
|
+
true
|
130
|
+
end
|
131
|
+
|
132
|
+
def warn?
|
133
|
+
true
|
134
|
+
end
|
135
|
+
|
136
|
+
def error?
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
def fatal?
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
def level
|
145
|
+
DEBUG
|
146
|
+
end
|
147
|
+
|
148
|
+
def tagged(*tags)
|
149
|
+
to_add = tags.flatten.compact
|
150
|
+
if block_given?
|
151
|
+
new_log_tags = current_tags
|
152
|
+
new_log_tags += to_add unless to_add.empty?
|
153
|
+
Prefab::Context.with_merged_context({ "log" => { "tags" => new_log_tags } }) do
|
154
|
+
with_context_keys LOG_TAGS do
|
155
|
+
yield self
|
156
|
+
end
|
157
|
+
end
|
158
|
+
else
|
159
|
+
new_log_tags = Prefab::Context.current.get(REQ_TAGS) || []
|
160
|
+
new_log_tags += to_add unless to_add.empty?
|
161
|
+
add_context_keys REQ_TAGS
|
162
|
+
Prefab::Context.current.set("req", {"tags": new_log_tags})
|
163
|
+
self
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def current_tags
|
168
|
+
Prefab::Context.current.get(LOG_TAGS) || []
|
169
|
+
end
|
170
|
+
|
171
|
+
def flush
|
172
|
+
Prefab::Context.current.set("req", {"tags": nil})
|
173
|
+
super if defined?(super)
|
174
|
+
end
|
175
|
+
|
176
|
+
def config_client=(config_client)
|
177
|
+
@config_client = config_client
|
178
|
+
end
|
179
|
+
|
180
|
+
def local_log_id
|
181
|
+
Thread.current.__id__
|
182
|
+
end
|
183
|
+
|
184
|
+
def silence
|
185
|
+
@silences[local_log_id] = true
|
186
|
+
yield self
|
187
|
+
ensure
|
188
|
+
@silences[local_log_id] = false
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
NO_DEFAULT = nil
|
194
|
+
|
195
|
+
def stringify_keys(hash)
|
196
|
+
Hash[hash.map { |k, v| [k.to_s, v] }]
|
197
|
+
end
|
198
|
+
|
199
|
+
def fetch_context_for_context_keys
|
200
|
+
context = Prefab::Context.current.to_h
|
201
|
+
Hash[context_keys.map do |key|
|
202
|
+
[key, context.dig(*key.split("."))]
|
203
|
+
end]
|
204
|
+
end
|
205
|
+
|
206
|
+
# Find the closest match to 'log_level.path' in config
|
207
|
+
def level_of(path)
|
208
|
+
closest_log_level_match = nil
|
209
|
+
|
210
|
+
path.split(SEP).each_with_object([BASE_KEY]) do |n, memo|
|
211
|
+
memo << n
|
212
|
+
val = @config_client.get(memo.join(SEP), NO_DEFAULT)
|
213
|
+
closest_log_level_match = val unless val.nil?
|
214
|
+
end
|
215
|
+
|
216
|
+
if closest_log_level_match.nil?
|
217
|
+
# get the top-level setting or default to WARN
|
218
|
+
closest_log_level_match = @config_client.get(BASE_KEY, :WARN)
|
219
|
+
end
|
220
|
+
|
221
|
+
closest_log_level_match_int = PrefabProto::LogLevel.resolve(closest_log_level_match)
|
222
|
+
LOG_LEVEL_LOOKUPS[closest_log_level_match_int]
|
223
|
+
end
|
224
|
+
|
225
|
+
def get_loc_path(loc)
|
226
|
+
loc_path = loc.absolute_path || loc.to_s
|
227
|
+
get_path(loc_path, loc.base_label)
|
228
|
+
end
|
229
|
+
|
230
|
+
# sanitize & clean the path of the caller so the key
|
231
|
+
# looks like log_level.app.models.user
|
232
|
+
def get_path(absolute_path, base_label)
|
233
|
+
path = (absolute_path || UNKNOWN_PATH).dup
|
234
|
+
path.slice! Dir.pwd
|
235
|
+
path.gsub!(%r{(.*)?(?=/lib)}im, '') # replace everything before first lib
|
236
|
+
|
237
|
+
path = path.gsub('/', SEP).gsub(/.rb.*/, '') + SEP + base_label
|
238
|
+
path.slice! '.lib'
|
239
|
+
path.slice! SEP
|
240
|
+
path
|
241
|
+
end
|
242
|
+
|
243
|
+
def format_message(severity, datetime, progname, msg, path = nil, log_context = {})
|
244
|
+
formatter = (@formatter || @default_formatter)
|
245
|
+
compact_context = log_context.reject{ |_, v| v.nil? || ((v.is_a? Array) && v.empty?) }
|
246
|
+
@formatter.call_proc(
|
247
|
+
severity: severity,
|
248
|
+
datetime: datetime,
|
249
|
+
progname: progname,
|
250
|
+
path: path,
|
251
|
+
message: msg,
|
252
|
+
log_context: compact_context
|
253
|
+
)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# StubConfigClient to be used while config client initializes
|
258
|
+
# since it may log
|
259
|
+
class BootstrappingConfigClient
|
260
|
+
def get(_key, default = nil, _properties = {})
|
261
|
+
ENV['PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'] ? ENV['PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'].upcase.to_sym : default
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Murmur3
|
4
|
+
## MurmurHash3 was written by Austin Appleby, and is placed in the public
|
5
|
+
## domain. The author hereby disclaims copyright to this source code.
|
6
|
+
|
7
|
+
MASK32 = 0xffffffff
|
8
|
+
|
9
|
+
def self.murmur3_32_rotl(x, r)
|
10
|
+
((x << r) | (x >> (32 - r))) & MASK32
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.murmur3_32_fmix(h)
|
14
|
+
h &= MASK32
|
15
|
+
h ^= h >> 16
|
16
|
+
h = (h * 0x85ebca6b) & MASK32
|
17
|
+
h ^= h >> 13
|
18
|
+
h = (h * 0xc2b2ae35) & MASK32
|
19
|
+
h ^ (h >> 16)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.murmur3_32__mmix(k1)
|
23
|
+
k1 = (k1 * 0xcc9e2d51) & MASK32
|
24
|
+
k1 = murmur3_32_rotl(k1, 15)
|
25
|
+
(k1 * 0x1b873593) & MASK32
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.murmur3_32(str, seed = 0)
|
29
|
+
h1 = seed
|
30
|
+
numbers = str.unpack('V*C*')
|
31
|
+
tailn = str.length % 4
|
32
|
+
tail = numbers.slice!(numbers.size - tailn, tailn)
|
33
|
+
for k1 in numbers
|
34
|
+
h1 ^= murmur3_32__mmix(k1)
|
35
|
+
h1 = murmur3_32_rotl(h1, 13)
|
36
|
+
h1 = (h1 * 5 + 0xe6546b64) & MASK32
|
37
|
+
end
|
38
|
+
|
39
|
+
unless tail.empty?
|
40
|
+
k1 = 0
|
41
|
+
tail.reverse_each do |c1|
|
42
|
+
k1 = (k1 << 8) | c1
|
43
|
+
end
|
44
|
+
h1 ^= murmur3_32__mmix(k1)
|
45
|
+
end
|
46
|
+
|
47
|
+
h1 ^= str.length
|
48
|
+
murmur3_32_fmix(h1)
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
# This class contains all the options that can be passed to the Prefab client.
|
5
|
+
class Options
|
6
|
+
attr_reader :api_key
|
7
|
+
attr_reader :logdev
|
8
|
+
attr_reader :log_prefix
|
9
|
+
attr_reader :log_formatter
|
10
|
+
attr_reader :namespace
|
11
|
+
attr_reader :prefab_api_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 :prefab_envs
|
18
|
+
attr_reader :collect_sync_interval
|
19
|
+
attr_reader :use_local_cache
|
20
|
+
attr_reader :datafile
|
21
|
+
attr_reader :disable_action_controller_logging
|
22
|
+
attr_accessor :is_fork
|
23
|
+
|
24
|
+
DEFAULT_LOG_FORMATTER = proc { |data|
|
25
|
+
severity = data[:severity]
|
26
|
+
datetime = data[:datetime]
|
27
|
+
progname = data[:progname]
|
28
|
+
path = data[:path]
|
29
|
+
msg = data[:message]
|
30
|
+
log_context = data[:log_context]
|
31
|
+
|
32
|
+
progname = (progname.nil? || progname.empty?) ? path : "#{progname}: #{path}"
|
33
|
+
|
34
|
+
formatted_log_context = log_context.sort.map do |k, v|
|
35
|
+
"#{k}=#{v}"
|
36
|
+
end.join(" ")
|
37
|
+
"#{severity.ljust(5)} #{datetime}:#{' ' if progname}#{progname} #{msg}#{log_context.any? ? " " + formatted_log_context : ""}\n"
|
38
|
+
}
|
39
|
+
|
40
|
+
JSON_LOG_FORMATTER = proc { |data|
|
41
|
+
log_context = data.delete(:log_context)
|
42
|
+
data.merge(log_context).compact.to_json << "\n"
|
43
|
+
}
|
44
|
+
|
45
|
+
COMPACT_LOG_FORMATTER = proc { |data|
|
46
|
+
severity = data[:severity]
|
47
|
+
msg = data[:message]
|
48
|
+
log_context = data[:log_context]
|
49
|
+
log_context["path"] = data[:path] || ""
|
50
|
+
|
51
|
+
formatted_log_context = log_context.sort.map do |k, v|
|
52
|
+
"#{k}=#{v}"
|
53
|
+
end.join(" ")
|
54
|
+
"#{severity.ljust(5)} #{msg&.strip} #{formatted_log_context}\n"
|
55
|
+
}
|
56
|
+
|
57
|
+
module ON_INITIALIZATION_FAILURE
|
58
|
+
RAISE = :raise
|
59
|
+
RETURN = :return
|
60
|
+
end
|
61
|
+
|
62
|
+
module ON_NO_DEFAULT
|
63
|
+
RAISE = :raise
|
64
|
+
RETURN_NIL = :return_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
module DATASOURCES
|
68
|
+
ALL = :all
|
69
|
+
LOCAL_ONLY = :local_only
|
70
|
+
end
|
71
|
+
|
72
|
+
DEFAULT_MAX_PATHS = 1_000
|
73
|
+
DEFAULT_MAX_KEYS = 100_000
|
74
|
+
DEFAULT_MAX_EXAMPLE_CONTEXTS = 100_000
|
75
|
+
DEFAULT_MAX_EVAL_SUMMARIES = 100_000
|
76
|
+
|
77
|
+
private def init(
|
78
|
+
api_key: ENV['PREFAB_API_KEY'],
|
79
|
+
logdev: $stdout,
|
80
|
+
namespace: '',
|
81
|
+
log_formatter: DEFAULT_LOG_FORMATTER,
|
82
|
+
log_prefix: nil,
|
83
|
+
prefab_api_url: ENV['PREFAB_API_URL'] || 'https://api.prefab.cloud',
|
84
|
+
on_no_default: ON_NO_DEFAULT::RAISE, # options :raise, :warn_and_return_nil,
|
85
|
+
initialization_timeout_sec: 10, # how long to wait before on_init_failure
|
86
|
+
on_init_failure: ON_INITIALIZATION_FAILURE::RAISE,
|
87
|
+
prefab_datasources: ENV['PREFAB_DATASOURCES'] == 'LOCAL_ONLY' ? DATASOURCES::LOCAL_ONLY : DATASOURCES::ALL,
|
88
|
+
prefab_config_override_dir: Dir.home,
|
89
|
+
prefab_config_classpath_dir: '.', # where to load local overrides
|
90
|
+
prefab_envs: ENV['PREFAB_ENVS'].nil? ? [] : ENV['PREFAB_ENVS'].split(','),
|
91
|
+
collect_logger_counts: true,
|
92
|
+
collect_max_paths: DEFAULT_MAX_PATHS,
|
93
|
+
collect_sync_interval: nil,
|
94
|
+
context_upload_mode: :periodic_example, # :periodic_example, :shape_only, :none
|
95
|
+
context_max_size: DEFAULT_MAX_EVAL_SUMMARIES,
|
96
|
+
collect_evaluation_summaries: true,
|
97
|
+
collect_max_evaluation_summaries: DEFAULT_MAX_EVAL_SUMMARIES,
|
98
|
+
allow_telemetry_in_local_mode: false,
|
99
|
+
x_datafile: ENV['PREFAB_DATAFILE'],
|
100
|
+
x_use_local_cache: false,
|
101
|
+
disable_action_controller_logging: false
|
102
|
+
)
|
103
|
+
@api_key = api_key
|
104
|
+
@logdev = logdev
|
105
|
+
@namespace = namespace
|
106
|
+
@log_formatter = log_formatter
|
107
|
+
@log_prefix = log_prefix
|
108
|
+
@prefab_api_url = remove_trailing_slash(prefab_api_url)
|
109
|
+
@on_no_default = on_no_default
|
110
|
+
@initialization_timeout_sec = initialization_timeout_sec
|
111
|
+
@on_init_failure = on_init_failure
|
112
|
+
@prefab_datasources = prefab_datasources
|
113
|
+
@datafile = x_datafile
|
114
|
+
@prefab_config_classpath_dir = prefab_config_classpath_dir
|
115
|
+
@prefab_config_override_dir = prefab_config_override_dir
|
116
|
+
@prefab_envs = Array(prefab_envs)
|
117
|
+
@collect_logger_counts = collect_logger_counts
|
118
|
+
@collect_max_paths = collect_max_paths
|
119
|
+
@collect_sync_interval = collect_sync_interval
|
120
|
+
@collect_evaluation_summaries = collect_evaluation_summaries
|
121
|
+
@collect_max_evaluation_summaries = collect_max_evaluation_summaries
|
122
|
+
@allow_telemetry_in_local_mode = allow_telemetry_in_local_mode
|
123
|
+
@use_local_cache = x_use_local_cache
|
124
|
+
@disable_action_controller_logging = disable_action_controller_logging
|
125
|
+
@is_fork = false
|
126
|
+
|
127
|
+
# defaults that may be overridden by context_upload_mode
|
128
|
+
@collect_shapes = false
|
129
|
+
@collect_max_shapes = 0
|
130
|
+
@collect_example_contexts = false
|
131
|
+
@collect_max_example_contexts = 0
|
132
|
+
|
133
|
+
case context_upload_mode
|
134
|
+
when :none
|
135
|
+
# do nothing
|
136
|
+
when :periodic_example
|
137
|
+
@collect_example_contexts = true
|
138
|
+
@collect_max_example_contexts = context_max_size
|
139
|
+
when :shape_only
|
140
|
+
@collect_shapes = true
|
141
|
+
@collect_max_shapes = context_max_size
|
142
|
+
else
|
143
|
+
raise "Unknown context_upload_mode #{context_upload_mode}. Please provide :periodic_example, :shape_only, or :none."
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def initialize(options = {})
|
148
|
+
init(**options)
|
149
|
+
end
|
150
|
+
|
151
|
+
def local_only?
|
152
|
+
@prefab_datasources == DATASOURCES::LOCAL_ONLY
|
153
|
+
end
|
154
|
+
|
155
|
+
def datafile?
|
156
|
+
!@datafile.nil?
|
157
|
+
end
|
158
|
+
|
159
|
+
def collect_max_paths
|
160
|
+
return 0 unless telemetry_allowed?(@collect_logger_counts)
|
161
|
+
|
162
|
+
@collect_max_paths
|
163
|
+
end
|
164
|
+
|
165
|
+
def collect_max_shapes
|
166
|
+
return 0 unless telemetry_allowed?(@collect_shapes)
|
167
|
+
|
168
|
+
@collect_max_shapes
|
169
|
+
end
|
170
|
+
|
171
|
+
def collect_max_example_contexts
|
172
|
+
return 0 unless telemetry_allowed?(@collect_example_contexts)
|
173
|
+
|
174
|
+
@collect_max_example_contexts
|
175
|
+
end
|
176
|
+
|
177
|
+
def collect_max_evaluation_summaries
|
178
|
+
return 0 unless telemetry_allowed?(@collect_evaluation_summaries)
|
179
|
+
|
180
|
+
@collect_max_evaluation_summaries
|
181
|
+
end
|
182
|
+
|
183
|
+
# https://api.prefab.cloud -> https://api-prefab-cloud.global.ssl.fastly.net
|
184
|
+
def url_for_api_cdn
|
185
|
+
ENV['PREFAB_CDN_URL'] || "#{@prefab_api_url.gsub(/\./, '-')}.global.ssl.fastly.net"
|
186
|
+
end
|
187
|
+
|
188
|
+
def api_key_id
|
189
|
+
@api_key&.split("-")&.first
|
190
|
+
end
|
191
|
+
|
192
|
+
def for_fork
|
193
|
+
clone = self.clone
|
194
|
+
clone.is_fork = true
|
195
|
+
clone
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def telemetry_allowed?(option)
|
201
|
+
option && (!local_only? || @allow_telemetry_in_local_mode)
|
202
|
+
end
|
203
|
+
|
204
|
+
def remove_trailing_slash(url)
|
205
|
+
url.end_with?('/') ? url[0..-2] : url
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
module PeriodicSync
|
5
|
+
LOG = Prefab::InternalLogger.new("periodsync")
|
6
|
+
def sync
|
7
|
+
return if @data.size.zero?
|
8
|
+
|
9
|
+
LOG.debug "Syncing #{@data.size} items"
|
10
|
+
|
11
|
+
start_at_was = @start_at
|
12
|
+
@start_at = Prefab::TimeHelpers.now_in_ms
|
13
|
+
|
14
|
+
flush(prepare_data, start_at_was)
|
15
|
+
end
|
16
|
+
|
17
|
+
def prepare_data
|
18
|
+
to_ship = @data.dup
|
19
|
+
@data.clear
|
20
|
+
|
21
|
+
on_prepare_data
|
22
|
+
|
23
|
+
to_ship
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_prepare_data
|
27
|
+
# noop -- override as you wish
|
28
|
+
end
|
29
|
+
|
30
|
+
def post(url, data)
|
31
|
+
@client.post(url, data)
|
32
|
+
end
|
33
|
+
|
34
|
+
def start_periodic_sync(sync_interval)
|
35
|
+
@start_at = Prefab::TimeHelpers.now_in_ms
|
36
|
+
|
37
|
+
@sync_interval = calculate_sync_interval(sync_interval)
|
38
|
+
|
39
|
+
Thread.new do
|
40
|
+
LOG.debug "Initialized #{@name} instance_hash=#{@client.instance_hash}"
|
41
|
+
|
42
|
+
loop do
|
43
|
+
sleep @sync_interval.call
|
44
|
+
sync
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def pool
|
50
|
+
@pool ||= Concurrent::ThreadPoolExecutor.new(
|
51
|
+
fallback_policy: :discard,
|
52
|
+
max_queue: 5,
|
53
|
+
max_threads: 4,
|
54
|
+
min_threads: 1,
|
55
|
+
name: @name
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def calculate_sync_interval(sync_interval)
|
62
|
+
if sync_interval.is_a?(Numeric)
|
63
|
+
proc { sync_interval }
|
64
|
+
else
|
65
|
+
sync_interval || ExponentialBackoff.new(initial_delay: 8, max_delay: 60 * 5)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
@@lock = Concurrent::ReadWriteLock.new
|
5
|
+
|
6
|
+
def self.init(options = Prefab::Options.new)
|
7
|
+
unless @singleton.nil?
|
8
|
+
Prefab::LoggerClient.instance.warn 'Prefab already initialized.'
|
9
|
+
return @singleton
|
10
|
+
end
|
11
|
+
|
12
|
+
@@lock.with_write_lock {
|
13
|
+
@singleton = Prefab::Client.new(options)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.fork
|
18
|
+
ensure_initialized
|
19
|
+
@@lock.with_write_lock {
|
20
|
+
@singleton = @singleton.fork
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.set_rails_loggers
|
25
|
+
ensure_initialized
|
26
|
+
@singleton.set_rails_loggers
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.get(key, properties = NO_DEFAULT_PROVIDED)
|
30
|
+
ensure_initialized key
|
31
|
+
@singleton.get(key, properties)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED)
|
35
|
+
ensure_initialized feature_name
|
36
|
+
@singleton.enabled?(feature_name, jit_context)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.with_context(properties, &block)
|
40
|
+
ensure_initialized
|
41
|
+
@singleton.with_context(properties, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.instance
|
45
|
+
ensure_initialized
|
46
|
+
@singleton
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def self.ensure_initialized(key = nil)
|
52
|
+
if not defined? @singleton or @singleton.nil?
|
53
|
+
raise Prefab::Errors::UninitializedError.new(key)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
# A key-based rate limiter that considers a key to be fresh if it has been
|
5
|
+
# seen within the last `duration` seconds.
|
6
|
+
#
|
7
|
+
# This is used to rate limit the number of times we send a given context
|
8
|
+
# to the server.
|
9
|
+
#
|
10
|
+
# Because expected usage is to immediately `set` on a `fresh?` miss, we do
|
11
|
+
# not prune the data structure on `fresh?` calls. Instead, we manually invoke
|
12
|
+
# `prune` periodically from the cache consumer.
|
13
|
+
class RateLimitCache
|
14
|
+
attr_reader :data
|
15
|
+
|
16
|
+
def initialize(duration)
|
17
|
+
@data = Concurrent::Map.new
|
18
|
+
@duration = duration
|
19
|
+
end
|
20
|
+
|
21
|
+
def fresh?(key)
|
22
|
+
timestamp = @data[key]
|
23
|
+
|
24
|
+
return false unless timestamp
|
25
|
+
return false if Time.now.utc.to_i - timestamp > @duration
|
26
|
+
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def set(key)
|
31
|
+
@data[key] = Time.now.utc.to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
def prune
|
35
|
+
now = Time.now.utc.to_i
|
36
|
+
@data.each_pair do |key, (timestamp, _)|
|
37
|
+
@data.delete(key) if now - timestamp > @duration
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|