prefab-cloud-ruby 1.0.1 → 1.1.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: eb91b8be5a01c3bab3312d277a27117c0449713623e1605863486c3809cff0cd
4
- data.tar.gz: 3deaeb9fcb458ef461771865463c5163f8b5c6833a45d1e7af4ec3474f5d04bd
3
+ metadata.gz: e3a735317d9c319aace8ce4cbc99c28a99ed83d23bda5a401c781a6e4e370c51
4
+ data.tar.gz: c9299c6f257d48b07f90a427d2e6b80b2db8c1daf5e3be5c92df34b3c13d76c3
5
5
  SHA512:
6
- metadata.gz: 2555fc0364fef33c11e3e7866a00ab5df766e859c73baedbb39712ddceb07ec96d0216c5e79dd7336e40951de85b773100864356fd24423f3257a8e3ab40e073
7
- data.tar.gz: 44c9813ede3c95db9820e0247c71c7e4a1e0e21b5981d074025baf546e5f3e7de6362744acedd831437e3b10ca747cdb73ec596ec1394b82ff7e70481ab57a52
6
+ metadata.gz: 722133d87e4a67ccce34e18d417b40e904da68e686b128badbde3d2a98a5b2b9e0a90f68c001de63cfc9877edf43982f4ffd59bc4dde0dcc6c10e43dc6a3cd83
7
+ data.tar.gz: 4673e84c936cf72f502402956b846ec14c8cbaaf90d2a90c794e0db4a352f8000cbb1477bd2d43abe685937879cdf6263dc63cfcf0218e7b15265876990c6a84
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 1.1.0 - 2023-09-18
6
+
7
+ - Add support for structured logging (#143)
8
+ - Ability to pass a hash of key/value context pairs to any of the user-facing log methods
9
+
10
+
3
11
  ## 1.0.1 - 2023-08-17
4
12
 
5
13
  - Bug fix for StringList w/ ExampleContextsAggregator (#141)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.1.0
data/lib/prefab/client.rb CHANGED
@@ -99,8 +99,8 @@ module Prefab
99
99
  resolver.on_update(&block)
100
100
  end
101
101
 
102
- def log_internal(level, msg, path = nil)
103
- log.log_internal msg, path, nil, level
102
+ def log_internal(level, msg, path = nil, **tags)
103
+ log.log_internal msg, path, nil, level, tags
104
104
  end
105
105
 
106
106
  def enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED)
@@ -54,7 +54,7 @@ module Prefab
54
54
  end
55
55
  )
56
56
 
57
- result = @client.post('/api/v1/context-shapes', shapes)
57
+ result = post('/api/v1/context-shapes', shapes)
58
58
 
59
59
  log_internal "Uploaded #{to_ship.values.size} shapes: #{result.status}"
60
60
  end
@@ -55,7 +55,7 @@ module Prefab
55
55
  summaries: summaries(to_ship)
56
56
  )
57
57
 
58
- result = @client.post('/api/v1/telemetry', events(summaries_proto))
58
+ result = post('/api/v1/telemetry', events(summaries_proto))
59
59
 
60
60
  log_internal "Uploaded #{to_ship.size} summaries: #{result.status}"
61
61
  end
@@ -45,7 +45,7 @@ module Prefab
45
45
  pool.post do
46
46
  log_internal "Flushing #{to_ship.size} examples"
47
47
 
48
- result = @client.post('/api/v1/telemetry', events(to_ship))
48
+ result = post('/api/v1/telemetry', events(to_ship))
49
49
 
50
50
  log_internal "Uploaded #{to_ship.size} examples: #{result.status}"
51
51
  end
@@ -25,6 +25,9 @@ module Prefab
25
25
 
26
26
  @data = Concurrent::Map.new
27
27
 
28
+ @last_data_sent = nil
29
+ @last_request = nil
30
+
28
31
  start_periodic_sync(sync_interval)
29
32
  end
30
33
 
@@ -55,7 +58,7 @@ module Prefab
55
58
  namespace: @client.namespace
56
59
  )
57
60
 
58
- result = @client.post('/api/v1/known-loggers', loggers)
61
+ result = post('/api/v1/known-loggers', loggers)
59
62
 
60
63
  log_internal "Uploaded #{to_ship.size} paths: #{result.status}"
61
64
  end
@@ -27,26 +27,26 @@ module Prefab
27
27
  @log_path_aggregator = log_path_aggregator
28
28
  end
29
29
 
30
- def add_internal(severity, message, progname, loc, &block)
30
+ def add_internal(severity, message, progname, loc, log_context={}, &block)
31
31
  path_loc = get_loc_path(loc)
32
32
  path = @prefix + path_loc
33
33
 
34
34
  @log_path_aggregator&.push(path_loc, severity)
35
35
 
36
- log(message, path, progname, severity, &block)
36
+ log(message, path, progname, severity, log_context, &block)
37
37
  end
38
38
 
39
- def log_internal(message, path, progname, severity, &block)
39
+ def log_internal(message, path, progname, severity, log_context={}, &block)
40
40
  path = if path
41
41
  "#{INTERNAL_PREFIX}.#{path}"
42
42
  else
43
43
  INTERNAL_PREFIX
44
44
  end
45
45
 
46
- log(message, path, progname, severity, &block)
46
+ log(message, path, progname, severity, log_context, &block)
47
47
  end
48
48
 
49
- def log(message, path, progname, severity)
49
+ def log(message, path, progname, severity, log_context={})
50
50
  severity ||= ::Logger::UNKNOWN
51
51
 
52
52
  return true if @logdev.nil? || severity < level_of(path) || @silences[local_log_id]
@@ -63,29 +63,29 @@ module Prefab
63
63
  end
64
64
 
65
65
  @logdev.write(
66
- format_message(format_severity(severity), Time.now, progname, message, path)
66
+ format_message(format_severity(severity), Time.now, progname, message, path, log_context)
67
67
  )
68
68
  true
69
69
  end
70
70
 
71
- def debug(progname = nil, &block)
72
- add_internal(DEBUG, nil, progname, caller_locations(1, 1)[0], &block)
71
+ def debug(progname = nil, **log_context, &block)
72
+ add_internal(DEBUG, nil, progname, caller_locations(1, 1)[0], log_context, &block)
73
73
  end
74
74
 
75
- def info(progname = nil, &block)
76
- add_internal(INFO, nil, progname, caller_locations(1, 1)[0], &block)
75
+ def info(progname = nil, **log_context, &block)
76
+ add_internal(INFO, nil, progname, caller_locations(1, 1)[0], log_context, &block)
77
77
  end
78
78
 
79
- def warn(progname = nil, &block)
80
- add_internal(WARN, nil, progname, caller_locations(1, 1)[0], &block)
79
+ def warn(progname = nil, **log_context, &block)
80
+ add_internal(WARN, nil, progname, caller_locations(1, 1)[0], log_context, &block)
81
81
  end
82
82
 
83
- def error(progname = nil, &block)
84
- add_internal(ERROR, nil, progname, caller_locations(1, 1)[0], &block)
83
+ def error(progname = nil, **log_context, &block)
84
+ add_internal(ERROR, nil, progname, caller_locations(1, 1)[0], log_context, &block)
85
85
  end
86
86
 
87
- def fatal(progname = nil, &block)
88
- add_internal(FATAL, nil, progname, caller_locations(1, 1)[0], &block)
87
+ def fatal(progname = nil, **log_context, &block)
88
+ add_internal(FATAL, nil, progname, caller_locations(1, 1)[0], log_context, &block)
89
89
  end
90
90
 
91
91
  def debug?
@@ -168,18 +168,17 @@ module Prefab
168
168
  path
169
169
  end
170
170
 
171
- def format_message(severity, datetime, progname, msg, path = nil)
171
+ def format_message(severity, datetime, progname, msg, path = nil, log_context={})
172
172
  formatter = (@formatter || @default_formatter)
173
173
 
174
- if formatter.arity == 5
175
- formatter.call(severity, datetime, progname, msg, path)
176
- else
177
- formatter.call(severity, datetime, join_path_and_progname(path, progname), msg)
178
- end
179
- end
180
-
181
- def join_path_and_progname(path, progname)
182
- (progname.nil? || progname.empty?) ? path : "#{progname}: #{path}"
174
+ formatter.call(
175
+ severity: severity,
176
+ datetime: datetime,
177
+ progname: progname,
178
+ path: path,
179
+ message: msg,
180
+ log_context: log_context
181
+ )
183
182
  end
184
183
  end
185
184
 
@@ -17,17 +17,23 @@ module Prefab
17
17
  attr_reader :prefab_envs
18
18
  attr_reader :collect_sync_interval
19
19
 
20
- DEFAULT_LOG_FORMATTER = proc { |severity, datetime, progname, msg|
21
- "#{severity.ljust(5)} #{datetime}:#{' ' if progname}#{progname} #{msg}\n"
20
+ DEFAULT_LOG_FORMATTER = proc { |data|
21
+ severity = data[:severity]
22
+ datetime = data[:datetime]
23
+ progname = data[:progname]
24
+ path = data[:path]
25
+ msg = data[:message]
26
+ log_context = data[:log_context]
27
+
28
+ progname = (progname.nil? || progname.empty?) ? path : "#{progname}: #{path}"
29
+
30
+ formatted_log_context = log_context.sort.map{|k, v| "#{k}=#{v}" }.join(" ")
31
+ "#{severity.ljust(5)} #{datetime}:#{' ' if progname}#{progname} #{msg}#{log_context.any? ? " " + formatted_log_context : ""}\n"
22
32
  }
23
- JSON_LOG_FORMATTER = proc { |severity, datetime, progname, msg, path|
24
- {
25
- type: severity,
26
- time: datetime,
27
- progname: progname,
28
- message: msg,
29
- path: path
30
- }.compact.to_json << "\n"
33
+
34
+ JSON_LOG_FORMATTER = proc { |data|
35
+ log_context = data.delete(:log_context)
36
+ data.merge(log_context).compact.to_json << "\n"
31
37
  }
32
38
 
33
39
  module ON_INITIALIZATION_FAILURE
@@ -26,6 +26,10 @@ module Prefab
26
26
  # noop -- override as you wish
27
27
  end
28
28
 
29
+ def post(url, data)
30
+ @client.post(url, data)
31
+ end
32
+
29
33
  def start_periodic_sync(sync_interval)
30
34
  @start_at = Prefab::TimeHelpers.now_in_ms
31
35
 
@@ -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.0.1 ruby lib
5
+ # stub: prefab-cloud-ruby 1.1.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "prefab-cloud-ruby".freeze
9
- s.version = "1.0.1"
9
+ s.version = "1.1.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-08-17"
14
+ s.date = "2023-09-18"
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]
@@ -1,18 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class IntegrationTest
4
- attr_reader :func, :input, :expected, :test_client
4
+ attr_reader :func, :input, :expected, :data, :expected_data, :aggregator, :endpoint, :test_client
5
5
 
6
6
  def initialize(test_data)
7
7
  @client_overrides = parse_client_overrides(test_data['client_overrides'])
8
8
  @func = parse_function(test_data['function'])
9
9
  @input = parse_input(test_data['input'])
10
10
  @expected = parse_expected(test_data['expected'])
11
- @test_client = base_client
11
+ @data = test_data['data']
12
+ @expected_data = test_data['expected_data']
13
+ @aggregator = test_data['aggregator']
14
+ @endpoint = test_data['endpoint']
15
+ @test_client = capture_telemetry(base_client)
12
16
  end
13
17
 
14
18
  def test_type
15
- if @input[0] && @input[0].start_with?('log-level.')
19
+ if @data
20
+ :telemetry
21
+ elsif @input[0] && @input[0].start_with?('log-level.')
16
22
  :log_level
17
23
  elsif @expected[:status] == 'raise'
18
24
  :raise
@@ -23,6 +29,18 @@ class IntegrationTest
23
29
  end
24
30
  end
25
31
 
32
+ def last_data_sent
33
+ test_client.last_data_sent
34
+ end
35
+
36
+ def last_post_result
37
+ test_client.last_post_result
38
+ end
39
+
40
+ def last_post_endpoint
41
+ test_client.last_post_endpoint
42
+ end
43
+
26
44
  private
27
45
 
28
46
  def parse_client_overrides(overrides)
@@ -42,6 +60,8 @@ class IntegrationTest
42
60
  end
43
61
 
44
62
  def parse_input(input)
63
+ return nil if input.nil?
64
+
45
65
  if input['key']
46
66
  parse_config_input(input)
47
67
  elsif input['flag']
@@ -62,6 +82,8 @@ class IntegrationTest
62
82
  end
63
83
 
64
84
  def parse_expected(expected)
85
+ return {} if expected.nil?
86
+
65
87
  {
66
88
  status: expected['status'],
67
89
  error: parse_error_type(expected['error']),
@@ -73,6 +95,7 @@ class IntegrationTest
73
95
  def parse_error_type(error_type)
74
96
  case error_type
75
97
  when 'missing_default' then Prefab::Errors::MissingDefaultError
98
+ when 'initialization_timeout' then Prefab::Errors::InitializationTimeoutError
76
99
  end
77
100
  end
78
101
 
@@ -87,7 +110,34 @@ class IntegrationTest
87
110
  prefab_envs: ['unit_tests'],
88
111
  prefab_datasources: Prefab::Options::DATASOURCES::ALL,
89
112
  api_key: ENV['PREFAB_INTEGRATION_TEST_API_KEY'],
90
- prefab_api_url: 'https://api.staging-prefab.cloud'
113
+ prefab_api_url: 'https://api.staging-prefab.cloud',
91
114
  }.merge(@client_overrides))
92
115
  end
116
+
117
+ def capture_telemetry(client)
118
+ client.define_singleton_method(:post) do |url, data|
119
+ client.instance_variable_set(:@last_data_sent, data)
120
+ client.instance_variable_set(:@last_post_endpoint, url)
121
+
122
+ result = super(url, data)
123
+
124
+ client.instance_variable_set(:@last_post_result, result)
125
+
126
+ result
127
+ end
128
+
129
+ client.define_singleton_method(:last_data_sent) do
130
+ client.instance_variable_get(:@last_data_sent)
131
+ end
132
+
133
+ client.define_singleton_method(:last_post_endpoint) do
134
+ client.instance_variable_get(:@last_post_endpoint)
135
+ end
136
+
137
+ client.define_singleton_method(:last_post_result) do
138
+ client.instance_variable_get(:@last_post_result)
139
+ end
140
+
141
+ client
142
+ end
93
143
  end
@@ -33,4 +33,117 @@ module IntegrationTestHelpers
33
33
  .select { |file| file =~ /\.ya?ml$/ }
34
34
  end
35
35
  end
36
+
37
+ def self.prepare_post_data(it)
38
+ case it.aggregator
39
+ when "log_path"
40
+ aggregator = it.test_client.log_path_aggregator
41
+
42
+ it.data.each do |(path, data)|
43
+ data.each_with_index do |count, severity|
44
+ count.times { aggregator.push(path, severity) }
45
+ end
46
+ end
47
+
48
+ expected_loggers = Hash.new { |h, k| h[k] = PrefabProto::Logger.new }
49
+
50
+ it.expected_data.each do |data|
51
+ data["counts"].each do |(severity, count)|
52
+ expected_loggers[data["logger_name"]][severity] = count
53
+ expected_loggers[data["logger_name"]]["logger_name"] = data["logger_name"]
54
+ end
55
+ end
56
+
57
+ [aggregator, ->(data) { data.loggers }, expected_loggers.values]
58
+ when "context_shape"
59
+ aggregator = it.test_client.context_shape_aggregator
60
+
61
+ context = Prefab::Context.new(it.data)
62
+
63
+ aggregator.push(context)
64
+
65
+ expected = it.expected_data.map do |data|
66
+ PrefabProto::ContextShape.new(
67
+ name: data["name"],
68
+ field_types: data["field_types"]
69
+ )
70
+ end
71
+
72
+ [aggregator, ->(data) { data.shapes }, expected]
73
+ when "evaluation_summary"
74
+ aggregator = it.test_client.evaluation_summary_aggregator
75
+
76
+ aggregator.instance_variable_set("@data", Concurrent::Hash.new)
77
+
78
+ it.data.each do |key|
79
+ it.test_client.get(key)
80
+ end
81
+
82
+ expected_data = []
83
+ it.expected_data.each do |data|
84
+ value = if data["value_type"] == "string_list"
85
+ PrefabProto::StringList.new(values: data["value"])
86
+ else
87
+ data["value"]
88
+ end
89
+ expected_data << PrefabProto::ConfigEvaluationSummary.new(
90
+ key: data["key"],
91
+ type: data["type"].to_sym,
92
+ counters: [
93
+ PrefabProto::ConfigEvaluationCounter.new(
94
+ count: data["count"],
95
+ config_id: 0,
96
+ selected_value: PrefabProto::ConfigValue.new(data["value_type"] => value),
97
+ config_row_index: data["summary"]["config_row_index"],
98
+ conditional_value_index: data["summary"]["conditional_value_index"] || 0,
99
+ weighted_value_index: data["summary"]["weighted_value_index"],
100
+ reason: :UNKNOWN
101
+ )
102
+ ]
103
+ )
104
+ end
105
+
106
+ [aggregator, ->(data) {
107
+ data.events[0].summaries.summaries.each { |e|
108
+ e.counters.each { |c|
109
+ c.config_id = 0
110
+ }
111
+ }
112
+ }, expected_data]
113
+ when "example_contexts"
114
+ aggregator = it.test_client.example_contexts_aggregator
115
+
116
+ it.data.each do |hash|
117
+ aggregator.record(Prefab::Context.new(hash))
118
+ end
119
+
120
+ expected_data = []
121
+ it.expected_data.each do |data|
122
+ expected_data << PrefabProto::ExampleContext.new(
123
+ timestamp: 0,
124
+ contextSet: PrefabProto::ContextSet.new(
125
+ contexts: data.map do |(k, vs)|
126
+ PrefabProto::Context.new(
127
+ type: k,
128
+ values: vs.map do |v|
129
+ [v["key"], PrefabProto::ConfigValue.new(v["value_type"] => v["value"])]
130
+ end.to_h
131
+ )
132
+ end
133
+ )
134
+ )
135
+ end
136
+ [aggregator, ->(data) { data.events[0].example_contexts.examples.each { |e| e.timestamp = 0 } }, expected_data]
137
+ else
138
+ puts "unknown aggregator #{it.aggregator}"
139
+ end
140
+ end
141
+
142
+ def self.with_parent_context_maybe(context, &block)
143
+ if context
144
+ Prefab::Context.with_context(context, &block)
145
+ else
146
+ yield
147
+ end
148
+ end
36
149
  end
@@ -82,6 +82,16 @@ module CommonHelpers
82
82
 
83
83
  FakeResponse = Struct.new(:status, :body)
84
84
 
85
+ def wait_for(condition, max_wait: 2, sleep_time: 0.01)
86
+ wait_time = 0
87
+ while !condition.call
88
+ wait_time += sleep_time
89
+ sleep sleep_time
90
+
91
+ raise "Waited #{max_wait} seconds for the condition to be true, but it never was" if wait_time > max_wait
92
+ end
93
+ end
94
+
85
95
  def wait_for_post_requests(client, max_wait: 2, sleep_time: 0.01)
86
96
  # we use ivars to avoid re-mocking the post method on subsequent calls
87
97
  client.instance_variable_set("@_requests", [])
@@ -99,13 +109,7 @@ module CommonHelpers
99
109
  yield
100
110
 
101
111
  # let the flush thread run
102
- wait_time = 0
103
- while client.instance_variable_get("@_requests").empty?
104
- wait_time += sleep_time
105
- sleep sleep_time
106
-
107
- raise "Waited #{max_wait} seconds for the flush thread to run, but it never did" if wait_time > max_wait
108
- end
112
+ wait_for -> { client.instance_variable_get("@_requests").size > 0 }, max_wait: max_wait, sleep_time: sleep_time
109
113
 
110
114
  client.instance_variable_get("@_requests")
111
115
  end
@@ -16,7 +16,7 @@ class TestIntegration < Minitest::Test
16
16
  define_method(:"test_#{test['name']}_#{test_case['name']}") do
17
17
  it = IntegrationTest.new(test_case)
18
18
 
19
- with_parent_context_maybe(parent_context) do
19
+ IntegrationTestHelpers.with_parent_context_maybe(parent_context) do
20
20
  case it.test_type
21
21
  when :raise
22
22
  err = assert_raises(it.expected[:error]) do
@@ -34,6 +34,19 @@ class TestIntegration < Minitest::Test
34
34
  end
35
35
  when :log_level
36
36
  assert_equal it.expected[:value].to_sym, it.test_client.send(it.func, *it.input)
37
+ when :telemetry
38
+ aggregator, get_actual_data, expected = IntegrationTestHelpers.prepare_post_data(it)
39
+ aggregator.sync
40
+
41
+ wait_for -> { it.last_post_result&.status == 200 }
42
+
43
+ assert it.endpoint == it.last_post_endpoint
44
+
45
+ actual = get_actual_data[it.last_data_sent]
46
+
47
+ expected.all? do |expected|
48
+ assert actual.include?(expected)
49
+ end
37
50
  else
38
51
  raise "Unknown test type: #{it.test_type}"
39
52
  end
@@ -42,14 +55,4 @@ class TestIntegration < Minitest::Test
42
55
  end
43
56
  end
44
57
  end
45
-
46
- private
47
-
48
- def with_parent_context_maybe(context, &block)
49
- if context
50
- Prefab::Context.with_context(context, &block)
51
- else
52
- yield
53
- end
54
- end
55
58
  end
data/test/test_logger.rb CHANGED
@@ -404,6 +404,46 @@ 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_structured_logging
408
+ prefab, io = captured_logger
409
+ message = 'HELLO'
410
+
411
+ prefab.log.error message, user: "michael", id: 123
412
+
413
+ assert_logged io, 'ERROR', 'test.test_logger.test_structured_logging', "#{message} id=123 user=michael"
414
+ end
415
+
416
+ def test_structured_json_logging
417
+ prefab, io = captured_logger(log_formatter: Prefab::Options::JSON_LOG_FORMATTER)
418
+ message = 'HELLO'
419
+
420
+ prefab.log.error message, user: "michael", id: 123
421
+
422
+ log_data = JSON.parse(io.string)
423
+ assert log_data["message"] == message
424
+ assert log_data["user"] == "michael"
425
+ assert log_data["id"] == 123
426
+ end
427
+
428
+ def test_structured_internal_logging
429
+ prefab, io = captured_logger
430
+
431
+ prefab.log.log_internal('test', 'test.path', '', ::Logger::WARN, user: "michael")
432
+
433
+ assert_logged io, 'WARN', 'cloud.prefab.client.test.path', "test user=michael"
434
+ end
435
+
436
+ def test_structured_block_logger
437
+ prefab, io = captured_logger
438
+ message = 'MY MESSAGE'
439
+
440
+ prefab.log.error user: "michael" do
441
+ message
442
+ end
443
+
444
+ assert_logged io, 'ERROR', 'test.test_logger.test_structured_block_logger', "#{message} user=michael"
445
+ end
446
+
407
447
  private
408
448
 
409
449
  def assert_logged(logged_io, level, path, message)
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.0.1
4
+ version: 1.1.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-08-17 00:00:00.000000000 Z
11
+ date: 2023-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby