prefab-cloud-ruby 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d3348f914c10f7f7d45814ff1be70a067e227cbef499b6f44736f10aa5cc30c
4
- data.tar.gz: 52699e5fee8f4f6ea26a89d3e3381cc6220d25d4720dcb69d7aee8de8f79e1c9
3
+ metadata.gz: 5bfaef7482dfdd1946bd0b0dee2cd0f366f5661e275bb8b906fb207d9b241d14
4
+ data.tar.gz: ed36c67d8039d2250fab5c6b1ff08da23cde51f696c256daaa943639205f2d0e
5
5
  SHA512:
6
- metadata.gz: c960267ea9f3e09664e48965fb470c1a3093061b1e6906f8f73b4c80a396ab80e0b383b3e42209e6cc95d5fab778c6221a2b9292a2015603139a874586eddf22
7
- data.tar.gz: 98cc728c27f1b1f6267c4b01b754d07f23fa60f8dfad95edd9e9288de666f6fc92539bf7124e85337023bf772946422c8919c75ebf487e9999a60251734a4d61
6
+ metadata.gz: 209f9cc828ec0eccd3b34162d9513c53695bf36838b60601103fd699748fad78af28cb622c1d057ea63162abd143f4db176395726a52517426885483789c7a3c
7
+ data.tar.gz: 3d368dc2aab1e941607e437d889dd51c8d5aaf9e65208d81552f930cfa34db0eff02fba1b7f2e7a3a08ab87a23e11fad67a0634861e586649fbc466a4f2d3629
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0 - 2023-10-30
4
+ - Add `Prefab.get('key')` style usage after a `Prefab.init()` call (#151)
5
+ - Add `add_context_keys` and `with_context_keys` method for LoggerClient (#145)
6
+
3
7
  ## 1.1.2 - 2023-10-13
4
8
 
5
9
  - Add `cloud.prefab.client.criteria_evaluator` `debug` logging of evaluations (#150)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.2
1
+ 1.2.0
data/lib/prefab/client.rb CHANGED
@@ -6,6 +6,7 @@ module Prefab
6
6
  class Client
7
7
  MAX_SLEEP_SEC = 10
8
8
  BASE_SLEEP_SEC = 0.5
9
+ LOG = Prefab::InternalLogger.new(Client)
9
10
 
10
11
  attr_reader :namespace, :interceptor, :api_key, :prefab_api_url, :options, :instance_hash
11
12
 
@@ -14,15 +15,19 @@ module Prefab
14
15
  @namespace = @options.namespace
15
16
  @stubs = {}
16
17
  @instance_hash = UUID.new.generate
18
+ Prefab::LoggerClient.new(@options.logdev, formatter: @options.log_formatter,
19
+ prefix: @options.log_prefix,
20
+ log_path_aggregator: log_path_aggregator
21
+ )
17
22
 
18
23
  if @options.local_only?
19
- log_internal ::Logger::DEBUG, 'Prefab Running in Local Mode'
24
+ LOG.debug 'Prefab Running in Local Mode'
20
25
  else
21
26
  @api_key = @options.api_key
22
27
  raise Prefab::Errors::InvalidApiKeyError, @api_key if @api_key.nil? || @api_key.empty? || api_key.count('-') < 1
23
28
 
24
29
  @prefab_api_url = @options.prefab_api_url
25
- log_internal ::Logger::DEBUG, "Prefab Connecting to: #{@prefab_api_url}"
30
+ LOG.debug "Prefab Connecting to: #{@prefab_api_url}"
26
31
  end
27
32
 
28
33
  context.clear
@@ -54,9 +59,7 @@ module Prefab
54
59
  end
55
60
 
56
61
  def log
57
- @logger_client ||= Prefab::LoggerClient.new(@options.logdev, formatter: @options.log_formatter,
58
- prefix: @options.log_prefix,
59
- log_path_aggregator: log_path_aggregator)
62
+ Prefab::LoggerClient.instance
60
63
  end
61
64
 
62
65
  def context_shape_aggregator
@@ -99,10 +102,6 @@ module Prefab
99
102
  resolver.on_update(&block)
100
103
  end
101
104
 
102
- def log_internal(level, msg, path = nil, **tags)
103
- log.log_internal msg, path, nil, level, tags
104
- end
105
-
106
105
  def enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED)
107
106
  feature_flag_client.feature_is_on_for?(feature_name, jit_context)
108
107
  end
@@ -133,7 +132,10 @@ module Prefab
133
132
  # $prefab.set_rails_loggers
134
133
  # end
135
134
  def fork
136
- Prefab::Client.new(@options.for_fork)
135
+ log_options = self.log.context_keys.to_a # get keys pre-fork
136
+ Prefab::Client.new(@options.for_fork).tap do |client|
137
+ client.log.add_context_keys(*log_options)
138
+ end
137
139
  end
138
140
 
139
141
  private
@@ -8,11 +8,12 @@ module Prefab
8
8
  STALE_CACHE_WARN_HOURS = 5
9
9
  AUTH_USER = 'authuser'
10
10
  LOGGING_KEY_PREFIX = "#{Prefab::LoggerClient::BASE_KEY}#{Prefab::LoggerClient::SEP}".freeze
11
+ LOG = Prefab::InternalLogger.new(ConfigClient)
11
12
 
12
13
  def initialize(base_client, timeout)
13
14
  @base_client = base_client
14
15
  @options = base_client.options
15
- @base_client.log_internal ::Logger::DEBUG, 'Initialize ConfigClient'
16
+ LOG.debug 'Initialize ConfigClient'
16
17
  @timeout = timeout
17
18
 
18
19
  @stream_lock = Concurrent::ReadWriteLock.new
@@ -23,9 +24,9 @@ module Prefab
23
24
  @config_resolver = Prefab::ConfigResolver.new(@base_client, @config_loader)
24
25
 
25
26
  @initialization_lock = Concurrent::ReadWriteLock.new
26
- @base_client.log_internal ::Logger::DEBUG, 'Initialize ConfigClient: AcquireWriteLock'
27
+ LOG.debug 'Initialize ConfigClient: AcquireWriteLock'
27
28
  @initialization_lock.acquire_write_lock
28
- @base_client.log_internal ::Logger::DEBUG, 'Initialize ConfigClient: AcquiredWriteLock'
29
+ LOG.debug 'Initialize ConfigClient: AcquiredWriteLock'
29
30
  @initialized_future = Concurrent::Future.execute { @initialization_lock.acquire_read_lock }
30
31
 
31
32
  if @options.local_only?
@@ -96,8 +97,7 @@ module Prefab
96
97
  raise Prefab::Errors::InitializationTimeoutError.new(@options.initialization_timeout_sec, key)
97
98
  end
98
99
 
99
- @base_client.log_internal ::Logger::WARN,
100
- "Couldn't Initialize In #{@options.initialization_timeout_sec}. Key #{key}. Returning what we have"
100
+ LOG.warn("Couldn't Initialize In #{@options.initialization_timeout_sec}. Key #{key}. Returning what we have")
101
101
  @initialization_lock.release_write_lock
102
102
  end
103
103
 
@@ -117,7 +117,7 @@ module Prefab
117
117
 
118
118
  return if success
119
119
 
120
- @base_client.log_internal ::Logger::WARN, 'No success loading checkpoints'
120
+ LOG.warn 'No success loading checkpoints'
121
121
  end
122
122
 
123
123
  def load_checkpoint_api_cdn
@@ -138,11 +138,11 @@ module Prefab
138
138
  cache_configs(configs)
139
139
  true
140
140
  else
141
- @base_client.log_internal ::Logger::INFO, "Checkpoint #{source} failed to load. Response #{resp.status}"
141
+ LOG.info "Checkpoint #{source} failed to load. Response #{resp.status}"
142
142
  false
143
143
  end
144
144
  rescue StandardError => e
145
- @base_client.log_internal ::Logger::WARN, "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
145
+ LOG.warn "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
146
146
  false
147
147
  end
148
148
 
@@ -167,11 +167,9 @@ module Prefab
167
167
  @config_loader.set(config, source)
168
168
  end
169
169
  if @config_loader.highwater_mark > starting_highwater_mark
170
- @base_client.log_internal ::Logger::DEBUG,
171
- "Found new checkpoint with highwater id #{@config_loader.highwater_mark} from #{source} in project #{project_id} environment: #{project_env_id} and namespace: '#{@namespace}'"
170
+ LOG.debug("Found new checkpoint with highwater id #{@config_loader.highwater_mark} from #{source} in project #{project_id} environment: #{project_env_id} and namespace: '#{@namespace}'")
172
171
  else
173
- @base_client.log_internal ::Logger::DEBUG,
174
- "Checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}. No changes.", 'load_configs'
172
+ LOG.debug("Checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}. No changes.")
175
173
  end
176
174
  @config_resolver.update
177
175
  finish_init!(source, project_id)
@@ -196,9 +194,9 @@ module Prefab
196
194
  f.flock(File::LOCK_EX)
197
195
  f.write(PrefabProto::Configs.encode_json(configs))
198
196
  end
199
- @base_client.log_internal ::Logger::DEBUG, "Cached configs to #{cache_path}"
197
+ LOG.debug "Cached configs to #{cache_path}"
200
198
  rescue => e
201
- @base_client.log_internal ::Logger::DEBUG, "Failed to cache configs to #{cache_path} #{e}"
199
+ LOG.debug "Failed to cache configs to #{cache_path} #{e}"
202
200
  end
203
201
 
204
202
  def load_cache
@@ -210,11 +208,11 @@ module Prefab
210
208
 
211
209
  hours_old = ((Time.now - File.mtime(f)) / 60 / 60).round(2)
212
210
  if hours_old > STALE_CACHE_WARN_HOURS
213
- @base_client.log_internal ::Logger::INFO, "Stale Cache Load: #{hours_old} hours old"
211
+ LOG.info "Stale Cache Load: #{hours_old} hours old"
214
212
  end
215
213
  end
216
214
  rescue => e
217
- @base_client.log_internal ::Logger::DEBUG, "Failed to read cached configs at #{cache_path}. #{e}"
215
+ LOG.debug "Failed to read cached configs at #{cache_path}. #{e}"
218
216
  false
219
217
  end
220
218
 
@@ -222,13 +220,13 @@ module Prefab
222
220
  def start_checkpointing_thread
223
221
  Thread.new do
224
222
  loop do
225
- load_checkpoint
226
-
227
223
  started_at = Time.now
228
224
  delta = @checkpoint_freq_secs - (Time.now - started_at)
229
225
  sleep(delta) if delta > 0
226
+
227
+ load_checkpoint
230
228
  rescue StandardError => e
231
- @base_client.log_internal ::Logger::DEBUG, "Issue Checkpointing #{e.message}"
229
+ LOG.debug "Issue Checkpointing #{e.message}"
232
230
  end
233
231
  end
234
232
  end
@@ -236,9 +234,10 @@ module Prefab
236
234
  def finish_init!(source, project_id)
237
235
  return unless @initialization_lock.write_locked?
238
236
 
239
- @base_client.log_internal ::Logger::DEBUG, "Unlocked Config via #{source}"
237
+ LOG.debug "Unlocked Config via #{source}"
240
238
  @initialization_lock.release_write_lock
241
- @base_client.log.config_client = self
239
+
240
+ Prefab::LoggerClient.instance.config_client = self
242
241
  presenter = Prefab::ConfigClientPresenter.new(
243
242
  size: @config_resolver.local_store.size,
244
243
  source: source,
@@ -246,8 +245,8 @@ module Prefab
246
245
  project_env_id: @config_resolver.project_env_id,
247
246
  api_key_id: @base_client.options.api_key_id
248
247
  )
249
- @base_client.log_internal ::Logger::INFO, presenter.to_s
250
- @base_client.log_internal ::Logger::DEBUG, to_s
248
+ LOG.info presenter.to_s
249
+ LOG.debug to_s
251
250
  end
252
251
 
253
252
  def start_sse_streaming_connection_thread(start_at_id)
@@ -259,11 +258,11 @@ module Prefab
259
258
  'X-PrefabCloud-Client-Version' => "prefab-cloud-ruby-#{Prefab::VERSION}"
260
259
  }
261
260
  url = "#{@base_client.prefab_api_url}/api/v1/sse/config"
262
- @base_client.log_internal ::Logger::DEBUG, "SSE Streaming Connect to #{url} start_at #{start_at_id}"
261
+ LOG.debug "SSE Streaming Connect to #{url} start_at #{start_at_id}"
263
262
  @streaming_thread = SSE::Client.new(url,
264
263
  headers: headers,
265
264
  read_timeout: SSE_READ_TIMEOUT,
266
- logger: Prefab::SseLogger.new(@base_client.log)) do |client|
265
+ logger: Prefab::SseLogger.new) do |client|
267
266
  client.on_event do |event|
268
267
  configs = PrefabProto::Configs.decode(Base64.decode64(event.data))
269
268
  load_configs(configs, :sse)
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Prefab
4
4
  class ConfigLoader
5
+ LOG = Prefab::InternalLogger.new(ConfigLoader)
6
+
5
7
  attr_reader :highwater_mark
6
8
 
7
9
  def initialize(base_client)
@@ -29,8 +31,8 @@ module Prefab
29
31
  @api_config.delete(config.key)
30
32
  else
31
33
  if @api_config[config.key]
32
- @base_client.log_internal ::Logger::DEBUG,
33
- "Replace #{config.key} with value from #{source} #{@api_config[config.key][:config].id} -> #{config.id}"
34
+ LOG.debug(
35
+ "Replace #{config.key} with value from #{source} #{@api_config[config.key][:config].id} -> #{config.id}")
34
36
  end
35
37
  @api_config[config.key] = { source: source, config: config }
36
38
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Prefab
4
4
  class ConfigValueUnwrapper
5
+ LOG = Prefab::InternalLogger.new(ConfigValueUnwrapper)
5
6
  attr_reader :value, :weighted_value_index
6
7
 
7
8
  def initialize(value, weighted_value_index = nil)
@@ -16,6 +17,7 @@ module Prefab
16
17
  when :string_list
17
18
  value.string_list.values
18
19
  else
20
+ LOG.error "Unknown type: #{config_value.type}"
19
21
  raise "Unknown type: #{config_value.type}"
20
22
  end
21
23
  end
@@ -6,6 +6,8 @@ module Prefab
6
6
  class ContextShapeAggregator
7
7
  include Prefab::PeriodicSync
8
8
 
9
+ LOG = Prefab::InternalLogger.new(ContextShapeAggregator)
10
+
9
11
  attr_reader :data
10
12
 
11
13
  def initialize(client:, max_shapes:, sync_interval:)
@@ -43,7 +45,7 @@ module Prefab
43
45
 
44
46
  def flush(to_ship, _)
45
47
  pool.post do
46
- log_internal "Uploading context shapes for #{to_ship.values.size}"
48
+ LOG.debug "Uploading context shapes for #{to_ship.values.size}"
47
49
 
48
50
  shapes = PrefabProto::ContextShapes.new(
49
51
  shapes: to_ship.map do |name, shape|
@@ -56,7 +58,7 @@ module Prefab
56
58
 
57
59
  result = post('/api/v1/context-shapes', shapes)
58
60
 
59
- log_internal "Uploaded #{to_ship.values.size} shapes: #{result.status}"
61
+ LOG.debug "Uploaded #{to_ship.values.size} shapes: #{result.status}"
60
62
  end
61
63
  end
62
64
  end
@@ -7,6 +7,7 @@ module Prefab
7
7
  # This class evaluates a config's criteria. `evaluate` returns the value of
8
8
  # the first match based on the provided properties.
9
9
  class CriteriaEvaluator
10
+ LOG = Prefab::InternalLogger.new(CriteriaEvaluator)
10
11
  NAMESPACE_KEY = 'NAMESPACE'
11
12
  NO_MATCHING_ROWS = [].freeze
12
13
 
@@ -21,7 +22,7 @@ module Prefab
21
22
  def evaluate(properties)
22
23
  rtn = evaluate_for_env(@project_env_id, properties) ||
23
24
  evaluate_for_env(0, properties)
24
- @base_client.log_internal ::Logger::DEBUG, "Eval Key #{@config.key} Result #{rtn&.value} with #{properties.to_h}", :criteria_evaluator unless @config.config_type == :LOG_LEVEL
25
+ LOG.debug "Eval Key #{@config.key} Result #{rtn&.value} with #{properties.to_h}" unless @config.config_type == :LOG_LEVEL
25
26
  rtn
26
27
  end
27
28
 
@@ -9,6 +9,8 @@ module Prefab
9
9
  class EvaluationSummaryAggregator
10
10
  include Prefab::PeriodicSync
11
11
 
12
+ LOG = Prefab::InternalLogger.new(EvaluationSummaryAggregator)
13
+
12
14
  attr_reader :data
13
15
 
14
16
  def initialize(client:, max_keys:, sync_interval:)
@@ -47,7 +49,7 @@ module Prefab
47
49
 
48
50
  def flush(to_ship, start_at_was)
49
51
  pool.post do
50
- log_internal "Flushing #{to_ship.size} summaries"
52
+ LOG.debug "Flushing #{to_ship.size} summaries"
51
53
 
52
54
  summaries_proto = PrefabProto::ConfigEvaluationSummaries.new(
53
55
  start: start_at_was,
@@ -57,7 +59,7 @@ module Prefab
57
59
 
58
60
  result = post('/api/v1/telemetry', events(summaries_proto))
59
61
 
60
- log_internal "Uploaded #{to_ship.size} summaries: #{result.status}"
62
+ LOG.debug "Uploaded #{to_ship.size} summaries: #{result.status}"
61
63
  end
62
64
  end
63
65
 
@@ -10,6 +10,8 @@ module Prefab
10
10
  class ExampleContextsAggregator
11
11
  include Prefab::PeriodicSync
12
12
 
13
+ LOG = Prefab::InternalLogger.new(ExampleContextsAggregator)
14
+
13
15
  attr_reader :data, :cache
14
16
 
15
17
  ONE_HOUR = 60 * 60
@@ -43,11 +45,11 @@ module Prefab
43
45
 
44
46
  def flush(to_ship, _)
45
47
  pool.post do
46
- log_internal "Flushing #{to_ship.size} examples"
48
+ LOG.debug "Flushing #{to_ship.size} examples"
47
49
 
48
50
  result = post('/api/v1/telemetry', events(to_ship))
49
51
 
50
- log_internal "Uploaded #{to_ship.size} examples: #{result.status}"
52
+ LOG.debug "Uploaded #{to_ship.size} examples: #{result.status}"
51
53
  end
52
54
  end
53
55
 
@@ -2,29 +2,32 @@
2
2
 
3
3
  module Prefab
4
4
  class InternalLogger < ::Logger
5
- def initialize(path, logger)
6
- @path = path
7
- @logger = logger
5
+ def initialize(path)
6
+ if path.is_a?(Class)
7
+ @path = path.name.split('::').last.downcase
8
+ else
9
+ @path = path
10
+ end
8
11
  end
9
12
 
10
- def debug(progname = nil)
11
- @logger.log_internal yield, @path, progname, DEBUG
13
+ def debug msg
14
+ Prefab::LoggerClient.instance.log_internal ::Logger::DEBUG, msg, @path
12
15
  end
13
16
 
14
- def info(progname = nil)
15
- @logger.log_internal yield, @path, progname, INFO
17
+ def info msg
18
+ Prefab::LoggerClient.instance.log_internal ::Logger::INFO, msg, @path
16
19
  end
17
20
 
18
- def warn(progname = nil)
19
- @logger.log_internal yield, @path, progname, WARN
21
+ def warn msg
22
+ Prefab::LoggerClient.instance.log_internal ::Logger::WARN, msg, @path
20
23
  end
21
24
 
22
- def error(progname = nil)
23
- @logger.log_internal yield, @path, progname, ERROR
25
+ def error msg
26
+ Prefab::LoggerClient.instance.log_internal ::Logger::ERROR, msg, @path
24
27
  end
25
28
 
26
- def fatal(progname = nil)
27
- @logger.log_internal yield, @path, progname, FATAL
29
+ def fatal msg
30
+ Prefab::LoggerClient.instance.log_internal ::Logger::FATAL, msg, @path
28
31
  end
29
32
  end
30
33
  end
@@ -4,6 +4,8 @@ require_relative 'periodic_sync'
4
4
 
5
5
  module Prefab
6
6
  class LogPathAggregator
7
+ LOG = Prefab::InternalLogger.new(LogPathAggregator)
8
+
7
9
  include Prefab::PeriodicSync
8
10
 
9
11
  INCREMENT = ->(count) { (count || 0) + 1 }
@@ -41,7 +43,7 @@ module Prefab
41
43
 
42
44
  def flush(to_ship, start_at_was)
43
45
  pool.post do
44
- log_internal "Uploading stats for #{to_ship.size} paths"
46
+ LOG.debug "Uploading stats for #{to_ship.size} paths"
45
47
 
46
48
  aggregate = Hash.new { |h, k| h[k] = PrefabProto::Logger.new }
47
49
 
@@ -60,7 +62,7 @@ module Prefab
60
62
 
61
63
  result = post('/api/v1/known-loggers', loggers)
62
64
 
63
- log_internal "Uploaded #{to_ship.size} paths: #{result.status}"
65
+ LOG.debug "Uploaded #{to_ship.size} paths: #{result.status}"
64
66
  end
65
67
  end
66
68
  end
@@ -17,7 +17,13 @@ module Prefab
17
17
  PrefabProto::LogLevel::FATAL => ::Logger::FATAL
18
18
  }.freeze
19
19
 
20
- def initialize(logdev, log_path_aggregator: nil, formatter: nil, prefix: nil)
20
+ def self.instance
21
+ @@shared_instance ||= LoggerClient.new($stdout)
22
+ end
23
+
24
+ attr_reader :context_keys
25
+
26
+ def initialize(logdev, log_path_aggregator: nil, formatter: Options::DEFAULT_LOG_FORMATTER, prefix: nil)
21
27
  super(logdev)
22
28
  self.formatter = formatter
23
29
  @config_client = BootstrappingConfigClient.new
@@ -25,9 +31,28 @@ module Prefab
25
31
  @recurse_check = Concurrent::Map.new(initial_capacity: 2)
26
32
  @prefix = "#{prefix}#{prefix && '.'}"
27
33
 
34
+ @context_keys = Concurrent::Set.new
35
+
28
36
  @log_path_aggregator = log_path_aggregator
37
+ @@shared_instance = self
29
38
  end
30
39
 
40
+ def add_context_keys(*keys)
41
+ @context_keys += keys
42
+ end
43
+
44
+ def with_context_keys(*keys)
45
+ @context_keys += keys
46
+ yield
47
+ ensure
48
+ @context_keys -= keys
49
+ end
50
+
51
+ def internal_logger(path=nil)
52
+ InternalLogger.new(path, self)
53
+ end
54
+
55
+ # InternalLoggers Will Call This
31
56
  def add_internal(severity, message, progname, loc, log_context={}, &block)
32
57
  path_loc = get_loc_path(loc)
33
58
  path = @prefix + path_loc
@@ -37,7 +62,7 @@ module Prefab
37
62
  log(message, path, progname, severity, log_context, &block)
38
63
  end
39
64
 
40
- def log_internal(message, path, progname, severity, log_context={}, &block)
65
+ def log_internal(severity, message, path, log_context={}, &block)
41
66
  return if @recurse_check[local_log_id]
42
67
  @recurse_check[local_log_id] = true
43
68
 
@@ -47,7 +72,7 @@ module Prefab
47
72
  INTERNAL_PREFIX
48
73
  end
49
74
  begin
50
- log(message, path, progname, severity, log_context, &block)
75
+ log(message, path, nil, severity, log_context, &block)
51
76
  ensure
52
77
  @recurse_check[local_log_id] = false
53
78
  end
@@ -55,7 +80,6 @@ module Prefab
55
80
 
56
81
  def log(message, path, progname, severity, log_context={})
57
82
  severity ||= ::Logger::UNKNOWN
58
-
59
83
  return true if @logdev.nil? || severity < level_of(path) || @silences[local_log_id]
60
84
 
61
85
  progname = @progname if progname.nil?
@@ -70,7 +94,7 @@ module Prefab
70
94
  end
71
95
 
72
96
  @logdev.write(
73
- format_message(format_severity(severity), Time.now, progname, message, path, log_context)
97
+ format_message(format_severity(severity), Time.now, progname, message, path, stringify_keys(log_context.merge(fetch_context_for_context_keys)))
74
98
  )
75
99
  true
76
100
  end
@@ -138,6 +162,17 @@ module Prefab
138
162
 
139
163
  NO_DEFAULT = nil
140
164
 
165
+ def stringify_keys(hash)
166
+ Hash[hash.map { |k, v| [k.to_s, v] }]
167
+ end
168
+
169
+ def fetch_context_for_context_keys
170
+ context = Prefab::Context.current.to_h
171
+ Hash[@context_keys.map do |key|
172
+ [key, context.dig(*key.split("."))]
173
+ end]
174
+ end
175
+
141
176
  # Find the closest match to 'log_level.path' in config
142
177
  def level_of(path)
143
178
  closest_log_level_match = nil
@@ -29,7 +29,9 @@ module Prefab
29
29
 
30
30
  progname = (progname.nil? || progname.empty?) ? path : "#{progname}: #{path}"
31
31
 
32
- formatted_log_context = log_context.sort.map{|k, v| "#{k}=#{v}" }.join(" ")
32
+ formatted_log_context = log_context.sort.map do |k, v|
33
+ v.nil? ? nil : "#{k}=#{v}"
34
+ end.compact.join(" ")
33
35
  "#{severity.ljust(5)} #{datetime}:#{' ' if progname}#{progname} #{msg}#{log_context.any? ? " " + formatted_log_context : ""}\n"
34
36
  }
35
37
 
@@ -2,10 +2,11 @@
2
2
 
3
3
  module Prefab
4
4
  module PeriodicSync
5
+ LOG = Prefab::InternalLogger.new("periodsync")
5
6
  def sync
6
7
  return if @data.size.zero?
7
8
 
8
- log_internal "Syncing #{@data.size} items"
9
+ LOG.debug "Syncing #{@data.size} items"
9
10
 
10
11
  start_at_was = @start_at
11
12
  @start_at = Prefab::TimeHelpers.now_in_ms
@@ -36,7 +37,7 @@ module Prefab
36
37
  @sync_interval = calculate_sync_interval(sync_interval)
37
38
 
38
39
  Thread.new do
39
- log_internal "Initialized #{@name} instance_hash=#{@client.instance_hash}"
40
+ LOG.debug "Initialized #{@name} instance_hash=#{@client.instance_hash}"
40
41
 
41
42
  loop do
42
43
  sleep @sync_interval.call
@@ -45,10 +46,6 @@ module Prefab
45
46
  end
46
47
  end
47
48
 
48
- def log_internal(message)
49
- @client.log.log_internal message, @name, nil, ::Logger::DEBUG
50
- end
51
-
52
49
  def pool
53
50
  @pool ||= Concurrent::ThreadPoolExecutor.new(
54
51
  fallback_policy: :discard,
@@ -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
31
+ @singleton.get(key, properties)
32
+ end
33
+
34
+ def self.enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED)
35
+ ensure_initialized
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
52
+ if not defined? @singleton or @singleton.nil?
53
+ raise "Use Prefab.initialize before calling Prefab.get"
54
+ end
55
+ end
56
+ end
@@ -1,14 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prefab
4
- class SseLogger < InternalLogger
5
- def initialize(logger)
6
- super('sse', logger)
4
+ class SseLogger < ::Logger
5
+ def initialize()
6
+ @path = "sse"
7
+ end
8
+
9
+ def debug(progname = nil, &block)
10
+ Prefab::LoggerClient.instance.log_internal ::Logger::DEBUG, progname, @path, &block
11
+ end
12
+
13
+ def info(progname = nil, &block)
14
+ Prefab::LoggerClient.instance.log_internal ::Logger::INFO, progname, @path, &block
7
15
  end
8
16
 
9
17
  # The SSE::Client warns on a perfectly normal stream disconnect, recast to info
10
- def warn(progname = nil)
11
- @logger.log_internal yield, @path, progname, INFO
18
+ def warn(progname = nil, &block)
19
+ Prefab::LoggerClient.instance.log_internal ::Logger::INFO, progname, @path, &block
20
+ end
21
+
22
+ def error(progname = nil, &block)
23
+ Prefab::LoggerClient.instance.log_internal ::Logger::ERROR, progname, @path, &block
24
+ end
25
+
26
+ def fatal(progname = nil, &block)
27
+ Prefab::LoggerClient.instance.log_internal ::Logger::FATAL, progname, @path, &block
12
28
  end
13
29
  end
14
30
  end
@@ -2,6 +2,8 @@ require 'yaml'
2
2
 
3
3
  module Prefab
4
4
  class YAMLConfigParser
5
+ LOG = Prefab::InternalLogger.new(YAMLConfigParser)
6
+
5
7
  def initialize(file, client)
6
8
  @file = file
7
9
  @client = client
@@ -21,10 +23,10 @@ module Prefab
21
23
 
22
24
  def load
23
25
  if File.exist?(@file)
24
- @client.log_internal ::Logger::INFO, "Load #{@file}"
26
+ LOG.info "Load #{@file}"
25
27
  YAML.load_file(@file)
26
28
  else
27
- @client.log_internal ::Logger::INFO, "No file #{@file}"
29
+ LOG.info "No file #{@file}"
28
30
  {}
29
31
  end
30
32
  end
@@ -43,4 +43,5 @@ require 'prefab/client'
43
43
  require 'prefab/config_client_presenter'
44
44
  require 'prefab/config_client'
45
45
  require 'prefab/feature_flag_client'
46
+ require 'prefab/prefab'
46
47
  require 'prefab/murmer3'
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: prefab-cloud-ruby 1.1.2 ruby lib
5
+ # stub: prefab-cloud-ruby 1.2.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "prefab-cloud-ruby".freeze
9
- s.version = "1.1.2"
9
+ s.version = "1.2.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Jeff Dwyer".freeze]
14
- s.date = "2023-10-13"
14
+ s.date = "2023-10-30"
15
15
  s.description = "Feature Flags, Live Config, and Dynamic Log Levels as a service".freeze
16
16
  s.email = "jdwyer@prefab.cloud".freeze
17
17
  s.executables = ["console".freeze]
@@ -65,6 +65,7 @@ Gem::Specification.new do |s|
65
65
  "lib/prefab/murmer3.rb",
66
66
  "lib/prefab/options.rb",
67
67
  "lib/prefab/periodic_sync.rb",
68
+ "lib/prefab/prefab.rb",
68
69
  "lib/prefab/rate_limit_cache.rb",
69
70
  "lib/prefab/resolved_config_presenter.rb",
70
71
  "lib/prefab/sse_logger.rb",
@@ -100,7 +101,9 @@ Gem::Specification.new do |s|
100
101
  "test/test_local_config_parser.rb",
101
102
  "test/test_log_path_aggregator.rb",
102
103
  "test/test_logger.rb",
104
+ "test/test_logger_initialization.rb",
103
105
  "test/test_options.rb",
106
+ "test/test_prefab.rb",
104
107
  "test/test_rate_limit_cache.rb",
105
108
  "test/test_weighted_value_resolver.rb"
106
109
  ]
@@ -44,24 +44,26 @@ module CommonHelpers
44
44
  }.freeze
45
45
 
46
46
  def new_client(overrides = {})
47
- $logs ||= StringIO.new
48
47
 
49
48
  config = overrides.delete(:config)
50
49
  project_env_id = overrides.delete(:project_env_id)
51
50
 
52
- options = Prefab::Options.new(
53
- **DEFAULT_NEW_CLIENT_OPTIONS.merge(
54
- overrides.merge(logdev: $logs)
55
- )
56
- )
57
-
58
- Prefab::Client.new(options).tap do |client|
51
+ Prefab::Client.new(prefab_options(overrides)).tap do |client|
59
52
  inject_config(client, config) if config
60
53
 
61
54
  client.resolver.project_env_id = project_env_id if project_env_id
62
55
  end
63
56
  end
64
57
 
58
+ def prefab_options(overrides = {})
59
+ $logs ||= StringIO.new
60
+ Prefab::Options.new(
61
+ **DEFAULT_NEW_CLIENT_OPTIONS.merge(
62
+ overrides.merge(logdev: $logs)
63
+ )
64
+ )
65
+ end
66
+
65
67
  def string_list(values)
66
68
  PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: values))
67
69
  end
@@ -145,7 +147,7 @@ module CommonHelpers
145
147
  end
146
148
 
147
149
  def assert_only_expected_logs
148
- assert_equal "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client No success loading checkpoints\n", $logs.string
150
+ assert_equal "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client.configclient No success loading checkpoints\n", $logs.string
149
151
  # mark nil to indicate we handled it
150
152
  $logs = nil
151
153
  end
@@ -9,8 +9,8 @@ class MockBaseClient
9
9
  def initialize(options = Prefab::Options.new)
10
10
  @options = options
11
11
  @namespace = namespace
12
- @logger = Prefab::LoggerClient.new($stdout)
13
12
  @config_client = MockConfigClient.new
13
+ Prefab::LoggerClient.new(options.logdev)
14
14
  @posts = []
15
15
  end
16
16
 
@@ -30,8 +30,6 @@ class MockBaseClient
30
30
  @logger
31
31
  end
32
32
 
33
- def log_internal(level, msg, path = nil, **tags); end
34
-
35
33
  def context_shape_aggregator; end
36
34
 
37
35
  def evaluation_summary_aggregator; end
data/test/test_client.rb CHANGED
@@ -185,7 +185,7 @@ class TestClient < Minitest::Test
185
185
  collect_evaluation_summaries: true).evaluation_summary_aggregator.class
186
186
 
187
187
  assert_logged [
188
- "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client No success loading checkpoints"
188
+ "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client.configclient No success loading checkpoints"
189
189
  ]
190
190
  end
191
191
 
@@ -395,7 +395,7 @@ class TestClient < Minitest::Test
395
395
  values: [
396
396
  PrefabProto::ConditionalValue.new(
397
397
  criteria: [PrefabProto::Criterion.new(operator: PrefabProto::Criterion::CriterionOperator::ALWAYS_TRUE)],
398
- value: PrefabProto::ConfigValue.new(log_level: PrefabProto::LogLevel::DEBUG)
398
+ value: PrefabProto::ConfigValue.new(log_level: PrefabProto::LogLevel::INFO)
399
399
  )
400
400
  ]
401
401
  )
@@ -405,12 +405,21 @@ class TestClient < Minitest::Test
405
405
  client = new_client(config: config, project_env_id: PROJECT_ENV_ID,
406
406
  collect_evaluation_summaries: true, allow_telemetry_in_local_mode: true)
407
407
 
408
- assert_equal :DEBUG, client.get(config.key, IRRELEVANT)
408
+ assert_equal :INFO, client.get(config.key, IRRELEVANT)
409
409
 
410
410
  # nothing is summarized for log levels
411
411
  assert_summary client, {}
412
412
  end
413
413
 
414
+ def test_fork_includes_logger_context_keys
415
+ client = new_client
416
+ client.log.add_context_keys "user.name"
417
+
418
+ forked = client.fork
419
+
420
+ assert forked.log.context_keys.to_a == %w(user.name)
421
+ end
422
+
414
423
  private
415
424
 
416
425
  def basic_value_config
@@ -124,8 +124,8 @@ class TestContextShapeAggregator < Minitest::Test
124
124
 
125
125
 
126
126
  assert_logged [
127
- "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client No success loading checkpoints",
128
- "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client Couldn't Initialize In 0. Key some.key. Returning what we have"
127
+ "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client.configclient No success loading checkpoints",
128
+ "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client.configclient Couldn't Initialize In 0. Key some.key. Returning what we have"
129
129
  ]
130
130
  end
131
131
 
@@ -710,22 +710,11 @@ class TestCriteriaEvaluator < Minitest::Test
710
710
  FakeResolver.new(config, @base_client)
711
711
  end
712
712
 
713
- class FakeLogger
714
- def info(msg)
715
- # loudly complain about unexpected log messages
716
- raise msg
717
- end
718
-
719
- def log_internal(*args); end
720
- end
721
-
722
713
  class FakeBaseClient
723
- def log
724
- FakeLogger.new
714
+ def initialize
715
+ Prefab::LoggerClient.new($stdout)
725
716
  end
726
717
 
727
- def log_internal(level, msg, path = nil, **tags); end
728
-
729
718
  def evaluation_summary_aggregator
730
719
  @evaluation_summary_aggregator ||= Prefab::EvaluationSummaryAggregator.new(client: self, max_keys: 9999, sync_interval: 9999)
731
720
  end
@@ -47,7 +47,7 @@ class TestLogPathAggregator < Minitest::Test
47
47
  ]], requests
48
48
 
49
49
  assert_logged [
50
- 'WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client No success loading checkpoints',
50
+ 'WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client.configclient No success loading checkpoints',
51
51
  'ERROR 2023-08-09 15:18:12 -0400: test.test_log_path_aggregator.test_sync here is a message'
52
52
  ]
53
53
  end
data/test/test_logger.rb CHANGED
@@ -87,22 +87,22 @@ class TestLogger < Minitest::Test
87
87
 
88
88
  def test_log_internal
89
89
  prefab, io = captured_logger
90
- prefab.log.log_internal('test message', 'test.path', '', ::Logger::WARN)
90
+ prefab.log.log_internal(::Logger::WARN, 'test message', 'test.path')
91
91
  assert_logged io, 'WARN', "cloud.prefab.client.test.path", "test message"
92
92
  end
93
93
 
94
94
  def test_log_internal_unknown
95
95
  prefab, io = captured_logger
96
- prefab.log.log_internal('test message', 'test.path', '', ::Logger::UNKNOWN)
96
+ prefab.log.log_internal(::Logger::UNKNOWN, 'test message', 'test.path')
97
97
  assert_logged io, 'ANY', "cloud.prefab.client.test.path", "test message"
98
98
  end
99
99
 
100
100
  def test_log_internal_silencing
101
101
  prefab, io = captured_logger
102
102
  prefab.log.silence do
103
- prefab.log.log_internal('should not log', 'test.path', '', ::Logger::WARN)
103
+ prefab.log.log_internal(::Logger::WARN, 'should not log', 'test.path')
104
104
  end
105
- prefab.log.log_internal('should log', 'test.path', '', ::Logger::WARN)
105
+ prefab.log.log_internal(::Logger::WARN, 'should log', 'test.path')
106
106
  assert_logged io, 'WARN', "cloud.prefab.client.test.path", "should log"
107
107
  refute_logged io, 'should not log'
108
108
  end
@@ -404,6 +404,35 @@ class TestLogger < Minitest::Test
404
404
  assert_logged io, 'ERROR', 'test.test_logger.test_logging_with_a_block', message
405
405
  end
406
406
 
407
+ def test_add_context_keys
408
+ assert @logger.context_keys.empty?
409
+ @logger.add_context_keys("user.name", "role.admin", "company.name")
410
+
411
+ assert @logger.context_keys.to_a == %w(user.name role.admin company.name)
412
+ end
413
+
414
+ def test_context_keys_are_a_set
415
+ @logger.add_context_keys("user.name", "role.admin", "company.name")
416
+
417
+ assert @logger.context_keys.to_a == %w(user.name role.admin company.name)
418
+
419
+ @logger.add_context_keys("user.name", "user.role")
420
+
421
+ assert @logger.context_keys.to_a == %w(user.name role.admin company.name user.role)
422
+ end
423
+
424
+ def test_with_context_keys
425
+ @logger.add_context_keys("company.name")
426
+
427
+ assert @logger.context_keys.to_a == %w(company.name)
428
+
429
+ @logger.with_context_keys("user.name", "role.admin") do
430
+ assert @logger.context_keys.to_a == %w(company.name user.name role.admin)
431
+ end
432
+
433
+ assert @logger.context_keys.to_a == %w(company.name)
434
+ end
435
+
407
436
  def test_structured_logging
408
437
  prefab, io = captured_logger
409
438
  message = 'HELLO'
@@ -428,7 +457,7 @@ class TestLogger < Minitest::Test
428
457
  def test_structured_internal_logging
429
458
  prefab, io = captured_logger
430
459
 
431
- prefab.log.log_internal('test', 'test.path', '', ::Logger::WARN, user: "michael")
460
+ prefab.log.log_internal(::Logger::WARN, 'test', 'test.path', user: "michael")
432
461
 
433
462
  assert_logged io, 'WARN', 'cloud.prefab.client.test.path', "test user=michael"
434
463
  end
@@ -444,6 +473,75 @@ class TestLogger < Minitest::Test
444
473
  assert_logged io, 'ERROR', 'test.test_logger.test_structured_block_logger', "#{message} user=michael"
445
474
  end
446
475
 
476
+ def test_structured_logger_with_context_keys
477
+ prefab, io = captured_logger
478
+
479
+ prefab.with_context({user: {name: "michael", job: "developer", admin: false}, company: { name: "Prefab" }}) do
480
+
481
+ prefab.log.add_context_keys "user.name", "company.name", "user.admin"
482
+
483
+ prefab.log.error "UH OH"
484
+
485
+ assert_logged io, 'ERROR', 'test.test_logger.test_structured_logger_with_context_keys',
486
+ "UH OH company.name=Prefab user.admin=false user.name=michael"
487
+ end
488
+ end
489
+
490
+ def test_structured_logger_with_context_keys_ignores_nils
491
+ prefab, io = captured_logger
492
+
493
+ prefab.with_context({user: {name: "michael", job: "developer"}, company: { name: "Prefab" }}) do
494
+
495
+ prefab.log.add_context_keys "user.name", "company.name", "user.admin"
496
+
497
+ prefab.log.error "UH OH"
498
+
499
+ assert_logged io, 'ERROR', 'test.test_logger.test_structured_logger_with_context_keys_ignores_nils',
500
+ "UH OH company.name=Prefab user.name=michael"
501
+ end
502
+ end
503
+
504
+ def test_structured_logger_with_context_keys_and_log_hash
505
+ prefab, io = captured_logger
506
+
507
+ prefab.with_context({user: {name: "michael", job: "developer", admin: false}, company: { name: "Prefab" }}) do
508
+
509
+ prefab.log.add_context_keys "user.name", "company.name", "user.admin"
510
+
511
+ prefab.log.error "UH OH", user_id: 6
512
+
513
+ assert_logged io, 'ERROR', 'test.test_logger.test_structured_logger_with_context_keys_and_log_hash',
514
+ "UH OH company.name=Prefab user.admin=false user.name=michael user_id=6"
515
+ end
516
+
517
+ end
518
+
519
+ def test_structured_logger_with_context_keys_block
520
+ prefab, io = captured_logger
521
+
522
+ prefab.with_context({user: {name: "michael", job: "developer", admin: false}, company: { name: "Prefab" }}) do
523
+
524
+ prefab.log.add_context_keys "user.name"
525
+
526
+ prefab.log.error "UH OH"
527
+
528
+ assert_logged io, 'ERROR', 'test.test_logger.test_structured_logger_with_context_keys_block',
529
+ 'UH OH user.name=michael'
530
+
531
+ prefab.log.with_context_keys("company.name") do
532
+ prefab.log.error "UH OH"
533
+
534
+ assert_logged io, 'ERROR', 'test.test_logger.test_structured_logger_with_context_keys_block',
535
+ 'UH OH company.name=Prefab user.name=michael'
536
+ end
537
+
538
+ prefab.log.error "UH OH"
539
+
540
+ assert_logged io, 'ERROR', 'test.test_logger.test_structured_logger_with_context_keys_block',
541
+ 'UH OH user.name=michael'
542
+ end
543
+ end
544
+
447
545
  private
448
546
 
449
547
  def assert_logged(logged_io, level, path, message)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestLoggerInitialization < Minitest::Test
6
+
7
+ def test_init_out_of_order
8
+ # assert nothing blows up
9
+ Prefab::LoggerClient.instance.info "anything"
10
+ end
11
+
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestPrefab < Minitest::Test
6
+ def test_get
7
+ Prefab.init(prefab_options)
8
+ assert_equal 'default', Prefab.get('does.not.exist', 'default')
9
+ assert_equal 'test sample value', Prefab.get('sample')
10
+ assert_equal 123, Prefab.get('sample_int')
11
+ end
12
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prefab-cloud-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Dwyer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-13 00:00:00.000000000 Z
11
+ date: 2023-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -224,6 +224,7 @@ files:
224
224
  - lib/prefab/murmer3.rb
225
225
  - lib/prefab/options.rb
226
226
  - lib/prefab/periodic_sync.rb
227
+ - lib/prefab/prefab.rb
227
228
  - lib/prefab/rate_limit_cache.rb
228
229
  - lib/prefab/resolved_config_presenter.rb
229
230
  - lib/prefab/sse_logger.rb
@@ -259,7 +260,9 @@ files:
259
260
  - test/test_local_config_parser.rb
260
261
  - test/test_log_path_aggregator.rb
261
262
  - test/test_logger.rb
263
+ - test/test_logger_initialization.rb
262
264
  - test/test_options.rb
265
+ - test/test_prefab.rb
263
266
  - test/test_rate_limit_cache.rb
264
267
  - test/test_weighted_value_resolver.rb
265
268
  homepage: http://github.com/prefab-cloud/prefab-cloud-ruby