sdk-reforge 1.9.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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/pull_request_template.md +8 -0
  5. data/.github/workflows/ruby.yml +48 -0
  6. data/.gitmodules +3 -0
  7. data/.rubocop.yml +13 -0
  8. data/.tool-versions +1 -0
  9. data/CHANGELOG.md +257 -0
  10. data/CODEOWNERS +1 -0
  11. data/Gemfile +29 -0
  12. data/Gemfile.lock +182 -0
  13. data/LICENSE.txt +20 -0
  14. data/README.md +105 -0
  15. data/Rakefile +63 -0
  16. data/VERSION +1 -0
  17. data/compile_protos.sh +20 -0
  18. data/dev/allocation_stats +60 -0
  19. data/dev/benchmark +40 -0
  20. data/dev/console +12 -0
  21. data/dev/script_setup.rb +18 -0
  22. data/lib/prefab_pb.rb +77 -0
  23. data/lib/reforge/caching_http_connection.rb +95 -0
  24. data/lib/reforge/client.rb +133 -0
  25. data/lib/reforge/config_client.rb +275 -0
  26. data/lib/reforge/config_client_presenter.rb +18 -0
  27. data/lib/reforge/config_loader.rb +67 -0
  28. data/lib/reforge/config_resolver.rb +84 -0
  29. data/lib/reforge/config_value_unwrapper.rb +123 -0
  30. data/lib/reforge/config_value_wrapper.rb +18 -0
  31. data/lib/reforge/context.rb +241 -0
  32. data/lib/reforge/context_shape.rb +20 -0
  33. data/lib/reforge/context_shape_aggregator.rb +70 -0
  34. data/lib/reforge/criteria_evaluator.rb +345 -0
  35. data/lib/reforge/duration.rb +58 -0
  36. data/lib/reforge/encryption.rb +65 -0
  37. data/lib/reforge/error.rb +6 -0
  38. data/lib/reforge/errors/env_var_parse_error.rb +11 -0
  39. data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
  40. data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
  41. data/lib/reforge/errors/missing_default_error.rb +13 -0
  42. data/lib/reforge/errors/missing_env_var_error.rb +11 -0
  43. data/lib/reforge/errors/uninitialized_error.rb +13 -0
  44. data/lib/reforge/evaluation.rb +53 -0
  45. data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
  46. data/lib/reforge/example_contexts_aggregator.rb +77 -0
  47. data/lib/reforge/exponential_backoff.rb +21 -0
  48. data/lib/reforge/feature_flag_client.rb +43 -0
  49. data/lib/reforge/fixed_size_hash.rb +14 -0
  50. data/lib/reforge/http_connection.rb +45 -0
  51. data/lib/reforge/internal_logger.rb +43 -0
  52. data/lib/reforge/javascript_stub.rb +99 -0
  53. data/lib/reforge/local_config_parser.rb +151 -0
  54. data/lib/reforge/murmer3.rb +50 -0
  55. data/lib/reforge/options.rb +191 -0
  56. data/lib/reforge/periodic_sync.rb +74 -0
  57. data/lib/reforge/prefab.rb +120 -0
  58. data/lib/reforge/rate_limit_cache.rb +41 -0
  59. data/lib/reforge/resolved_config_presenter.rb +86 -0
  60. data/lib/reforge/semver.rb +132 -0
  61. data/lib/reforge/sse_config_client.rb +112 -0
  62. data/lib/reforge/time_helpers.rb +7 -0
  63. data/lib/reforge/weighted_value_resolver.rb +42 -0
  64. data/lib/reforge/yaml_config_parser.rb +34 -0
  65. data/lib/reforge-sdk.rb +57 -0
  66. data/test/fixtures/datafile.json +87 -0
  67. data/test/integration_test.rb +171 -0
  68. data/test/integration_test_helpers.rb +114 -0
  69. data/test/support/common_helpers.rb +201 -0
  70. data/test/support/mock_base_client.rb +41 -0
  71. data/test/support/mock_config_client.rb +19 -0
  72. data/test/support/mock_config_loader.rb +1 -0
  73. data/test/test_caching_http_connection.rb +218 -0
  74. data/test/test_client.rb +351 -0
  75. data/test/test_config_client.rb +84 -0
  76. data/test/test_config_loader.rb +82 -0
  77. data/test/test_config_resolver.rb +502 -0
  78. data/test/test_config_value_unwrapper.rb +270 -0
  79. data/test/test_config_value_wrapper.rb +42 -0
  80. data/test/test_context.rb +271 -0
  81. data/test/test_context_shape.rb +50 -0
  82. data/test/test_context_shape_aggregator.rb +150 -0
  83. data/test/test_criteria_evaluator.rb +1180 -0
  84. data/test/test_duration.rb +37 -0
  85. data/test/test_encryption.rb +16 -0
  86. data/test/test_evaluation_summary_aggregator.rb +162 -0
  87. data/test/test_example_contexts_aggregator.rb +233 -0
  88. data/test/test_exponential_backoff.rb +18 -0
  89. data/test/test_feature_flag_client.rb +16 -0
  90. data/test/test_fixed_size_hash.rb +119 -0
  91. data/test/test_helper.rb +17 -0
  92. data/test/test_integration.rb +75 -0
  93. data/test/test_internal_logger.rb +25 -0
  94. data/test/test_javascript_stub.rb +176 -0
  95. data/test/test_local_config_parser.rb +147 -0
  96. data/test/test_logger_initialization.rb +12 -0
  97. data/test/test_options.rb +93 -0
  98. data/test/test_prefab.rb +16 -0
  99. data/test/test_rate_limit_cache.rb +44 -0
  100. data/test/test_semver.rb +108 -0
  101. data/test/test_sse_config_client.rb +211 -0
  102. data/test/test_weighted_value_resolver.rb +71 -0
  103. metadata +345 -0
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ class Duration
5
+ PATTERN = /P(?:(?<days>\d+(?:\.\d+)?)D)?(?:T(?:(?<hours>\d+(?:\.\d+)?)H)?(?:(?<minutes>\d+(?:\.\d+)?)M)?(?:(?<seconds>\d+(?:\.\d+)?)S)?)?/
6
+ MINUTES_IN_SECONDS = 60
7
+ HOURS_IN_SECONDS = 60 * MINUTES_IN_SECONDS
8
+ DAYS_IN_SECONDS = 24 * HOURS_IN_SECONDS
9
+
10
+ def initialize(definition)
11
+ @seconds = self.class.parse(definition)
12
+ end
13
+
14
+ def self.parse(definition)
15
+ match = PATTERN.match(definition)
16
+ return 0 unless match
17
+
18
+ days = match[:days]&.to_f || 0
19
+ hours = match[:hours]&.to_f || 0
20
+ minutes = match[:minutes]&.to_f || 0
21
+ seconds = match[:seconds]&.to_f || 0
22
+
23
+ (days * DAYS_IN_SECONDS + hours * HOURS_IN_SECONDS + minutes * MINUTES_IN_SECONDS + seconds)
24
+ end
25
+
26
+ def in_seconds
27
+ @seconds
28
+ end
29
+
30
+ def in_minutes
31
+ in_seconds / 60.0
32
+ end
33
+
34
+ def in_hours
35
+ in_minutes / 60.0
36
+ end
37
+
38
+ def in_days
39
+ in_hours / 24.0
40
+ end
41
+
42
+ def in_weeks
43
+ in_days / 7.0
44
+ end
45
+
46
+ def to_i
47
+ in_seconds.to_i
48
+ end
49
+
50
+ def to_f
51
+ in_seconds.to_f
52
+ end
53
+
54
+ def as_json
55
+ { ms: in_seconds * 1000, seconds: in_seconds }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ class Encryption
5
+ CIPHER_TYPE = "aes-256-gcm" # 32/12
6
+ SEPARATOR = "--"
7
+
8
+ # Hexadecimal format ensures that generated keys are representable with
9
+ # plain text
10
+ #
11
+ # To convert back to the original string with the desired length:
12
+ # [ value ].pack("H*")
13
+ def self.generate_new_hex_key
14
+ generate_random_key.unpack("H*")[0]
15
+ end
16
+
17
+ def initialize(key_string_hex)
18
+ @key = [key_string_hex].pack("H*")
19
+ end
20
+
21
+ def encrypt(clear_text)
22
+ cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
23
+ cipher.encrypt
24
+ iv = cipher.random_iv
25
+
26
+ # load them into the cipher
27
+ cipher.key = @key
28
+ cipher.iv = iv
29
+ cipher.auth_data = ""
30
+
31
+ # encrypt the message
32
+ encrypted = cipher.update(clear_text)
33
+ encrypted << cipher.final
34
+ tag = cipher.auth_tag
35
+
36
+ # pack and join
37
+ [encrypted, iv, tag].map { |p| p.unpack("H*")[0] }.join(SEPARATOR)
38
+ end
39
+
40
+ def decrypt(encrypted_string)
41
+ unpacked_parts = encrypted_string.split(SEPARATOR).map { |p| [p].pack("H*") }
42
+
43
+ cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
44
+ cipher.decrypt
45
+ cipher.key = @key
46
+ cipher.iv = unpacked_parts[1]
47
+ cipher.auth_tag = unpacked_parts[2]
48
+
49
+ # and decrypt it
50
+ decrypted = cipher.update(unpacked_parts[0])
51
+ decrypted << cipher.final
52
+ decrypted
53
+ end
54
+
55
+ private
56
+
57
+ def self.generate_random_key
58
+ SecureRandom.random_bytes(key_length)
59
+ end
60
+
61
+ def self.key_length
62
+ OpenSSL::Cipher.new(CIPHER_TYPE).key_len
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ module Errors
5
+ class EnvVarParseError < Reforge::Error
6
+ def initialize(env_var, config, env_var_name)
7
+ super("Evaluating #{config.key} couldn't coerce #{env_var_name} of #{env_var} to #{config.value_type}")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ module Errors
5
+ class InitializationTimeoutError < Reforge::Error
6
+ def initialize(timeout_sec, key)
7
+ message = "Reforge couldn't initialize in #{timeout_sec} second timeout. Trying to fetch key `#{key}`."
8
+ super(message)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ module Errors
5
+ class InvalidSdkKeyError < Reforge::Error
6
+ def initialize(key)
7
+ if key.nil? || key.empty?
8
+ message = 'No SDK key. Set REFORGE_SDK_KEY env var or use PREFAB_DATASOURCES=LOCAL_ONLY'
9
+
10
+ super(message)
11
+ else
12
+ message = "Your SDK key format is invalid. Expecting something like 123-development-yourapikey-SDK. You provided `#{key}`"
13
+
14
+ super(message)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ module Errors
5
+ class MissingDefaultError < Reforge::Error
6
+ def initialize(key)
7
+ message = "No value found for key '#{key}' and no default was provided.\n\nIf you'd prefer returning `nil` rather than raising when this occurs, modify the `on_no_default` value you provide in your Reforge::Options."
8
+
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ module Errors
5
+ class MissingEnvVarError < Reforge::Error
6
+ def initialize(message)
7
+ super(message)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ module Errors
5
+ class UninitializedError < Reforge::Error
6
+ def initialize(key=nil)
7
+ message = "Use Reforge.initialize before calling Reforge.get #{key}"
8
+
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ # Records the result of evaluating a config's criteria and forensics for reporting
5
+ class Evaluation
6
+ attr_reader :value, :context
7
+
8
+ def initialize(config:, value:, value_index:, config_row_index:, context:, resolver:)
9
+ @config = config
10
+ @value = value
11
+ @value_index = value_index
12
+ @config_row_index = config_row_index
13
+ @context = context
14
+ @resolver = resolver
15
+ end
16
+
17
+ def unwrapped_value
18
+ deepest_value.unwrap
19
+ end
20
+
21
+ def reportable_value
22
+ deepest_value.reportable_value
23
+ end
24
+
25
+ def report_and_return(evaluation_summary_aggregator)
26
+ report(evaluation_summary_aggregator)
27
+
28
+ unwrapped_value
29
+ end
30
+
31
+ private
32
+
33
+ def report(evaluation_summary_aggregator)
34
+ return if @config.config_type == :LOG_LEVEL
35
+
36
+ evaluation_summary_aggregator&.record(
37
+ config_key: @config.key,
38
+ config_type: @config.config_type,
39
+ counter: {
40
+ config_id: @config.id,
41
+ config_row_index: @config_row_index,
42
+ conditional_value_index: @value_index,
43
+ selected_value: deepest_value.reportable_wrapped_value,
44
+ weighted_value_index: deepest_value.weighted_value_index,
45
+ selected_index: nil # TODO
46
+ })
47
+ end
48
+
49
+ def deepest_value
50
+ @deepest_value ||= Reforge::ConfigValueUnwrapper.deepest_value(@value, @config, @context, @resolver)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'periodic_sync'
4
+
5
+ module Reforge
6
+ # This class aggregates the number of times each config is evaluated, and
7
+ # details about how the config is evaluated This data is reported to the
8
+ # server at a regular interval defined by `sync_interval`.
9
+ class EvaluationSummaryAggregator
10
+ include Reforge::PeriodicSync
11
+ LOG = Reforge::InternalLogger.new(self)
12
+
13
+ attr_reader :data
14
+
15
+ def initialize(client:, max_keys:, sync_interval:)
16
+ @client = client
17
+ @max_keys = max_keys
18
+ @name = 'evaluation_summary_aggregator'
19
+
20
+ @data = Concurrent::Hash.new
21
+
22
+ start_periodic_sync(sync_interval)
23
+ end
24
+
25
+ def record(config_key:, config_type:, counter:)
26
+ return if @data.size >= @max_keys
27
+
28
+ key = [config_key, config_type]
29
+ @data[key] ||= Concurrent::Hash.new
30
+
31
+ @data[key][counter] ||= 0
32
+ @data[key][counter] += 1
33
+ end
34
+
35
+ private
36
+
37
+ def counter_proto(counter, count)
38
+ PrefabProto::ConfigEvaluationCounter.new(
39
+ config_id: counter[:config_id],
40
+ selected_index: counter[:selected_index],
41
+ config_row_index: counter[:config_row_index],
42
+ conditional_value_index: counter[:conditional_value_index],
43
+ weighted_value_index: counter[:weighted_value_index],
44
+ selected_value: counter[:selected_value],
45
+ count: count
46
+ )
47
+ end
48
+
49
+ def flush(to_ship, start_at_was)
50
+ pool.post do
51
+ LOG.debug "Flushing #{to_ship.size} summaries"
52
+
53
+ summaries_proto = PrefabProto::ConfigEvaluationSummaries.new(
54
+ start: start_at_was,
55
+ end: Reforge::TimeHelpers.now_in_ms,
56
+ summaries: summaries(to_ship)
57
+ )
58
+
59
+ result = post('/api/v1/telemetry', events(summaries_proto))
60
+
61
+ LOG.debug "Uploaded #{to_ship.size} summaries: #{result.status}"
62
+ end
63
+ end
64
+
65
+ def events(summaries)
66
+ event = PrefabProto::TelemetryEvent.new(summaries: summaries)
67
+
68
+ PrefabProto::TelemetryEvents.new(
69
+ instance_hash: @client.instance_hash,
70
+ events: [event]
71
+ )
72
+ end
73
+
74
+ def summaries(data)
75
+ data.map do |(config_key, config_type), counters|
76
+ counter_protos = counters.map { |counter, count| counter_proto(counter, count) }
77
+
78
+ PrefabProto::ConfigEvaluationSummary.new(
79
+ key: config_key,
80
+ type: config_type,
81
+ counters: counter_protos
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'periodic_sync'
4
+
5
+ module Reforge
6
+ # This class aggregates example contexts. It dedupes based on the
7
+ # concatenation of the keys of the contexts.
8
+ #
9
+ # It shouldn't send the same context more than once per hour.
10
+ class ExampleContextsAggregator
11
+ include Reforge::PeriodicSync
12
+ LOG = Reforge::InternalLogger.new(self)
13
+
14
+ attr_reader :data, :cache
15
+
16
+ ONE_HOUR = 60 * 60
17
+
18
+ def initialize(client:, max_contexts:, sync_interval:)
19
+ @client = client
20
+ @max_contexts = max_contexts
21
+ @name = 'example_contexts_aggregator'
22
+
23
+ @data = Concurrent::Array.new
24
+ @cache = Reforge::RateLimitCache.new(ONE_HOUR)
25
+
26
+ start_periodic_sync(sync_interval)
27
+ end
28
+
29
+ def record(contexts)
30
+ key = contexts.grouped_key
31
+
32
+ return unless @data.size < @max_contexts && !@cache.fresh?(key)
33
+
34
+ @cache.set(key)
35
+
36
+ @data.push(contexts)
37
+ end
38
+
39
+ private
40
+
41
+ def on_prepare_data
42
+ @cache.prune
43
+ end
44
+
45
+ def flush(to_ship, _)
46
+ pool.post do
47
+ LOG.debug "Flushing #{to_ship.size} examples"
48
+
49
+ result = post('/api/v1/telemetry', events(to_ship))
50
+
51
+ LOG.debug "Uploaded #{to_ship.size} examples: #{result.status}"
52
+ end
53
+ end
54
+
55
+ def example_contexts(to_ship)
56
+ to_ship.map do |contexts|
57
+ PrefabProto::ExampleContext.new(
58
+ timestamp: contexts.seen_at * 1000,
59
+ contextSet: contexts.slim_proto
60
+ )
61
+ end
62
+ end
63
+
64
+ def events(to_ship)
65
+ event = PrefabProto::TelemetryEvent.new(
66
+ example_contexts: PrefabProto::ExampleContexts.new(
67
+ examples: example_contexts(to_ship)
68
+ )
69
+ )
70
+
71
+ PrefabProto::TelemetryEvents.new(
72
+ instance_hash: @client.instance_hash,
73
+ events: [event]
74
+ )
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ # This class implements exponential backoff with a maximum delay.
5
+ #
6
+ # This is the default sync interval for aggregators.
7
+ class ExponentialBackoff
8
+ def initialize(max_delay:, initial_delay: 2, multiplier: 2)
9
+ @initial_delay = initial_delay
10
+ @max_delay = max_delay
11
+ @multiplier = multiplier
12
+ @delay = initial_delay
13
+ end
14
+
15
+ def call
16
+ delay = @delay
17
+ @delay = [@delay * @multiplier, @max_delay].min
18
+ delay
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ class FeatureFlagClient
5
+ LOG = Reforge::InternalLogger.new(self)
6
+ def initialize(base_client)
7
+ @base_client = base_client
8
+ end
9
+
10
+ def feature_is_on?(feature_name)
11
+ feature_is_on_for?(feature_name, {})
12
+ end
13
+
14
+ def feature_is_on_for?(feature_name, properties)
15
+ variant = @base_client.config_client.get(feature_name, false, properties)
16
+
17
+ is_on?(variant)
18
+ end
19
+
20
+ def get(feature_name, properties, default: false)
21
+ value = _get(feature_name, properties)
22
+
23
+ value.nil? ? default : value
24
+ end
25
+
26
+ private
27
+
28
+ def _get(feature_name, properties)
29
+ @base_client.config_client.get(feature_name, nil, properties)
30
+ end
31
+
32
+ def is_on?(variant)
33
+ return false if variant.nil?
34
+
35
+ return variant if variant == !!variant
36
+
37
+ variant.bool
38
+ rescue StandardError
39
+ LOG.info("is_on? methods only work for boolean feature flags variants. This feature flags variant is '#{variant}'. Returning false")
40
+ false
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ module Reforge
3
+ class FixedSizeHash < Hash
4
+ def initialize(max_size)
5
+ @max_size = max_size
6
+ super()
7
+ end
8
+
9
+ def []=(key, value)
10
+ shift if size >= @max_size && !key?(key) # Only evict if adding a new key
11
+ super
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ class HttpConnection
5
+ AUTH_USER = 'authuser'
6
+ PROTO_HEADERS = {
7
+ 'Content-Type' => 'application/x-protobuf',
8
+ 'Accept' => 'application/x-protobuf',
9
+ 'X-Reforge-SDK-Version' => "sdk-ruby-#{Reforge::VERSION}"
10
+ }.freeze
11
+
12
+ def initialize(uri, sdk_key)
13
+ @uri = uri
14
+ @sdk_key = sdk_key
15
+ end
16
+
17
+ def uri
18
+ @uri
19
+ end
20
+
21
+ def get(path, headers = {})
22
+ connection(PROTO_HEADERS.merge(headers)).get(path)
23
+ end
24
+
25
+ def post(path, body)
26
+ connection(PROTO_HEADERS).post(path, body.to_proto)
27
+ end
28
+
29
+ def connection(headers = {})
30
+ if Faraday::VERSION[0].to_i >= 2
31
+ Faraday.new(@uri) do |conn|
32
+ conn.request :authorization, :basic, AUTH_USER, @sdk_key
33
+
34
+ conn.headers.merge!(headers)
35
+ end
36
+ else
37
+ Faraday.new(@uri) do |conn|
38
+ conn.request :basic_auth, AUTH_USER, @sdk_key
39
+
40
+ conn.headers.merge!(headers)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ module Reforge
2
+ class InternalLogger < SemanticLogger::Logger
3
+
4
+ def initialize(klass)
5
+ default_level = ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'] ? ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'].downcase.to_sym : :warn
6
+ super(klass, default_level)
7
+ instances << self
8
+ end
9
+
10
+ def log(log, message = nil, progname = nil, &block)
11
+ return if recurse_check[local_log_id]
12
+ recurse_check[local_log_id] = true
13
+ begin
14
+ super(log, message, progname, &block)
15
+ ensure
16
+ recurse_check[local_log_id] = false
17
+ end
18
+ end
19
+
20
+ def local_log_id
21
+ Thread.current.__id__
22
+ end
23
+
24
+ # Our client outputs debug logging,
25
+ # but if you aren't using Reforge logging this could be too chatty.
26
+ # If you aren't using reforge log filter, only log warn level and above
27
+ def self.using_reforge_log_filter!
28
+ @@instances.each do |l|
29
+ l.level = :trace
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def instances
36
+ @@instances ||= []
37
+ end
38
+
39
+ def recurse_check
40
+ @recurse_check ||=Concurrent::Map.new(initial_capacity: 2)
41
+ end
42
+ end
43
+ end