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.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/workflows/ruby.yml +46 -0
  4. data/.gitmodules +3 -0
  5. data/.rubocop.yml +13 -0
  6. data/.tool-versions +1 -0
  7. data/CHANGELOG.md +169 -0
  8. data/CODEOWNERS +1 -0
  9. data/Gemfile +26 -0
  10. data/Gemfile.lock +188 -0
  11. data/LICENSE.txt +20 -0
  12. data/README.md +94 -0
  13. data/Rakefile +50 -0
  14. data/VERSION +1 -0
  15. data/bin/console +21 -0
  16. data/compile_protos.sh +18 -0
  17. data/lib/prefab/client.rb +153 -0
  18. data/lib/prefab/config_client.rb +292 -0
  19. data/lib/prefab/config_client_presenter.rb +18 -0
  20. data/lib/prefab/config_loader.rb +84 -0
  21. data/lib/prefab/config_resolver.rb +77 -0
  22. data/lib/prefab/config_value_unwrapper.rb +115 -0
  23. data/lib/prefab/config_value_wrapper.rb +18 -0
  24. data/lib/prefab/context.rb +179 -0
  25. data/lib/prefab/context_shape.rb +20 -0
  26. data/lib/prefab/context_shape_aggregator.rb +65 -0
  27. data/lib/prefab/criteria_evaluator.rb +136 -0
  28. data/lib/prefab/encryption.rb +65 -0
  29. data/lib/prefab/error.rb +6 -0
  30. data/lib/prefab/errors/env_var_parse_error.rb +11 -0
  31. data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
  32. data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
  33. data/lib/prefab/errors/missing_default_error.rb +13 -0
  34. data/lib/prefab/errors/missing_env_var_error.rb +11 -0
  35. data/lib/prefab/errors/uninitialized_error.rb +13 -0
  36. data/lib/prefab/evaluation.rb +52 -0
  37. data/lib/prefab/evaluation_summary_aggregator.rb +87 -0
  38. data/lib/prefab/example_contexts_aggregator.rb +78 -0
  39. data/lib/prefab/exponential_backoff.rb +21 -0
  40. data/lib/prefab/feature_flag_client.rb +42 -0
  41. data/lib/prefab/http_connection.rb +41 -0
  42. data/lib/prefab/internal_logger.rb +16 -0
  43. data/lib/prefab/local_config_parser.rb +151 -0
  44. data/lib/prefab/log_path_aggregator.rb +69 -0
  45. data/lib/prefab/logger_client.rb +264 -0
  46. data/lib/prefab/murmer3.rb +50 -0
  47. data/lib/prefab/options.rb +208 -0
  48. data/lib/prefab/periodic_sync.rb +69 -0
  49. data/lib/prefab/prefab.rb +56 -0
  50. data/lib/prefab/rate_limit_cache.rb +41 -0
  51. data/lib/prefab/resolved_config_presenter.rb +86 -0
  52. data/lib/prefab/time_helpers.rb +7 -0
  53. data/lib/prefab/weighted_value_resolver.rb +42 -0
  54. data/lib/prefab/yaml_config_parser.rb +34 -0
  55. data/lib/prefab-cloud-ruby.rb +57 -0
  56. data/lib/prefab_pb.rb +93 -0
  57. data/prefab-cloud-ruby.gemspec +155 -0
  58. data/test/.prefab.default.config.yaml +2 -0
  59. data/test/.prefab.unit_tests.config.yaml +28 -0
  60. data/test/integration_test.rb +150 -0
  61. data/test/integration_test_helpers.rb +151 -0
  62. data/test/support/common_helpers.rb +180 -0
  63. data/test/support/mock_base_client.rb +42 -0
  64. data/test/support/mock_config_client.rb +19 -0
  65. data/test/support/mock_config_loader.rb +1 -0
  66. data/test/test_client.rb +444 -0
  67. data/test/test_config_client.rb +109 -0
  68. data/test/test_config_loader.rb +117 -0
  69. data/test/test_config_resolver.rb +430 -0
  70. data/test/test_config_value_unwrapper.rb +224 -0
  71. data/test/test_config_value_wrapper.rb +42 -0
  72. data/test/test_context.rb +203 -0
  73. data/test/test_context_shape.rb +50 -0
  74. data/test/test_context_shape_aggregator.rb +147 -0
  75. data/test/test_criteria_evaluator.rb +726 -0
  76. data/test/test_encryption.rb +16 -0
  77. data/test/test_evaluation_summary_aggregator.rb +162 -0
  78. data/test/test_example_contexts_aggregator.rb +238 -0
  79. data/test/test_exponential_backoff.rb +18 -0
  80. data/test/test_feature_flag_client.rb +48 -0
  81. data/test/test_helper.rb +17 -0
  82. data/test/test_integration.rb +58 -0
  83. data/test/test_local_config_parser.rb +147 -0
  84. data/test/test_log_path_aggregator.rb +62 -0
  85. data/test/test_logger.rb +621 -0
  86. data/test/test_logger_initialization.rb +12 -0
  87. data/test/test_options.rb +75 -0
  88. data/test/test_prefab.rb +12 -0
  89. data/test/test_rate_limit_cache.rb +44 -0
  90. data/test/test_weighted_value_resolver.rb +71 -0
  91. 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