quonfig 0.0.2

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 (108) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/rules/constitution.md +81 -0
  3. data/.claude/rules/git-safety.md +11 -0
  4. data/.claude/rules/issue-tracking.md +13 -0
  5. data/.claude/rules/testing-workflow.md +28 -0
  6. data/.envrc.sample +3 -0
  7. data/.github/CODEOWNERS +2 -0
  8. data/.github/pull_request_template.md +8 -0
  9. data/.github/workflows/push_gem.yml +49 -0
  10. data/.github/workflows/ruby.yml +60 -0
  11. data/.github/workflows/test.yaml +40 -0
  12. data/.rubocop.yml +13 -0
  13. data/.tool-versions +1 -0
  14. data/CHANGELOG.md +301 -0
  15. data/CLAUDE.md +29 -0
  16. data/CODEOWNERS +1 -0
  17. data/Gemfile +26 -0
  18. data/Gemfile.lock +177 -0
  19. data/LICENSE.txt +20 -0
  20. data/README.md +213 -0
  21. data/Rakefile +64 -0
  22. data/VERSION +1 -0
  23. data/dev/allocation_stats +60 -0
  24. data/dev/benchmark +40 -0
  25. data/dev/console +12 -0
  26. data/dev/script_setup.rb +18 -0
  27. data/lib/quonfig/bound_client.rb +71 -0
  28. data/lib/quonfig/caching_http_connection.rb +95 -0
  29. data/lib/quonfig/client.rb +221 -0
  30. data/lib/quonfig/config_envelope.rb +5 -0
  31. data/lib/quonfig/config_loader.rb +103 -0
  32. data/lib/quonfig/config_store.rb +42 -0
  33. data/lib/quonfig/context.rb +101 -0
  34. data/lib/quonfig/datadir.rb +101 -0
  35. data/lib/quonfig/duration.rb +58 -0
  36. data/lib/quonfig/encryption.rb +74 -0
  37. data/lib/quonfig/error.rb +6 -0
  38. data/lib/quonfig/errors/env_var_parse_error.rb +11 -0
  39. data/lib/quonfig/errors/initialization_timeout_error.rb +12 -0
  40. data/lib/quonfig/errors/invalid_sdk_key_error.rb +19 -0
  41. data/lib/quonfig/errors/missing_default_error.rb +13 -0
  42. data/lib/quonfig/errors/missing_env_var_error.rb +11 -0
  43. data/lib/quonfig/errors/type_mismatch_error.rb +11 -0
  44. data/lib/quonfig/errors/uninitialized_error.rb +13 -0
  45. data/lib/quonfig/evaluation.rb +64 -0
  46. data/lib/quonfig/evaluator.rb +464 -0
  47. data/lib/quonfig/exponential_backoff.rb +21 -0
  48. data/lib/quonfig/fixed_size_hash.rb +14 -0
  49. data/lib/quonfig/http_connection.rb +46 -0
  50. data/lib/quonfig/internal_logger.rb +173 -0
  51. data/lib/quonfig/murmer3.rb +50 -0
  52. data/lib/quonfig/options.rb +194 -0
  53. data/lib/quonfig/periodic_sync.rb +74 -0
  54. data/lib/quonfig/quonfig.rb +58 -0
  55. data/lib/quonfig/rate_limit_cache.rb +41 -0
  56. data/lib/quonfig/reason.rb +39 -0
  57. data/lib/quonfig/resolver.rb +42 -0
  58. data/lib/quonfig/semantic_logger_filter.rb +90 -0
  59. data/lib/quonfig/semver.rb +132 -0
  60. data/lib/quonfig/sse_config_client.rb +135 -0
  61. data/lib/quonfig/time_helpers.rb +7 -0
  62. data/lib/quonfig/types.rb +56 -0
  63. data/lib/quonfig/weighted_value_resolver.rb +49 -0
  64. data/lib/quonfig.rb +57 -0
  65. data/quonfig.gemspec +149 -0
  66. data/scripts/generate_integration_tests.rb +362 -0
  67. data/test/fixtures/datafile.json +87 -0
  68. data/test/integration/test_context_precedence.rb +194 -0
  69. data/test/integration/test_datadir_environment.rb +76 -0
  70. data/test/integration/test_enabled.rb +784 -0
  71. data/test/integration/test_enabled_with_contexts.rb +94 -0
  72. data/test/integration/test_get.rb +224 -0
  73. data/test/integration/test_get_feature_flag.rb +34 -0
  74. data/test/integration/test_get_or_raise.rb +86 -0
  75. data/test/integration/test_get_weighted_values.rb +29 -0
  76. data/test/integration/test_helpers.rb +139 -0
  77. data/test/integration/test_helpers_test.rb +73 -0
  78. data/test/integration/test_post.rb +34 -0
  79. data/test/integration/test_telemetry.rb +114 -0
  80. data/test/support/common_helpers.rb +106 -0
  81. data/test/support/mock_base_client.rb +27 -0
  82. data/test/support/mock_config_loader.rb +1 -0
  83. data/test/test_bound_client.rb +109 -0
  84. data/test/test_caching_http_connection.rb +218 -0
  85. data/test/test_client.rb +255 -0
  86. data/test/test_config_loader.rb +70 -0
  87. data/test/test_context.rb +136 -0
  88. data/test/test_datadir.rb +199 -0
  89. data/test/test_duration.rb +37 -0
  90. data/test/test_encryption.rb +16 -0
  91. data/test/test_evaluator.rb +285 -0
  92. data/test/test_exponential_backoff.rb +44 -0
  93. data/test/test_fixed_size_hash.rb +119 -0
  94. data/test/test_helper.rb +17 -0
  95. data/test/test_http_connection.rb +79 -0
  96. data/test/test_internal_logger.rb +34 -0
  97. data/test/test_options.rb +167 -0
  98. data/test/test_rate_limit_cache.rb +44 -0
  99. data/test/test_reason.rb +79 -0
  100. data/test/test_rename.rb +65 -0
  101. data/test/test_resolver.rb +144 -0
  102. data/test/test_semantic_logger_filter.rb +123 -0
  103. data/test/test_semver.rb +108 -0
  104. data/test/test_sse_config_client.rb +297 -0
  105. data/test/test_typed_getters.rb +131 -0
  106. data/test/test_types.rb +141 -0
  107. data/test/test_weighted_value_resolver.rb +84 -0
  108. metadata +311 -0
data/Gemfile ADDED
@@ -0,0 +1,26 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'concurrent-ruby', '~> 1.0', '>= 1.0.5'
4
+ gem 'faraday'
5
+ gem 'ld-eventsource'
6
+ gem 'uuid'
7
+
8
+ gem 'activesupport', '>= 4'
9
+
10
+ group :development do
11
+ gem 'allocation_stats'
12
+ gem 'benchmark-ips'
13
+ gem 'bundler'
14
+ gem 'juwelier', '~> 2.4.9'
15
+ gem 'rdoc'
16
+ gem 'simplecov', '>= 0'
17
+ end
18
+
19
+ group :test do
20
+ gem 'semantic_logger', '!= 4.16.0', require: "semantic_logger/sync"
21
+ gem 'minitest'
22
+ gem 'minitest-focus'
23
+ gem 'minitest-reporters'
24
+ gem 'timecop'
25
+ gem 'webrick'
26
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,177 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ activesupport (7.1.3.2)
5
+ base64
6
+ bigdecimal
7
+ concurrent-ruby (~> 1.0, >= 1.0.2)
8
+ connection_pool (>= 2.2.5)
9
+ drb
10
+ i18n (>= 1.6, < 2)
11
+ minitest (>= 5.1)
12
+ mutex_m
13
+ tzinfo (~> 2.0)
14
+ addressable (2.8.6)
15
+ public_suffix (>= 2.0.2, < 6.0)
16
+ allocation_stats (0.1.5)
17
+ ansi (1.5.0)
18
+ base64 (0.2.0)
19
+ benchmark-ips (2.13.0)
20
+ bigdecimal (3.1.7)
21
+ builder (3.2.4)
22
+ concurrent-ruby (1.2.3)
23
+ connection_pool (2.4.1)
24
+ descendants_tracker (0.0.4)
25
+ thread_safe (~> 0.3, >= 0.3.1)
26
+ docile (1.4.0)
27
+ domain_name (0.6.20240107)
28
+ drb (2.2.1)
29
+ faraday (1.10.3)
30
+ faraday-em_http (~> 1.0)
31
+ faraday-em_synchrony (~> 1.0)
32
+ faraday-excon (~> 1.1)
33
+ faraday-httpclient (~> 1.0)
34
+ faraday-multipart (~> 1.0)
35
+ faraday-net_http (~> 1.0)
36
+ faraday-net_http_persistent (~> 1.0)
37
+ faraday-patron (~> 1.0)
38
+ faraday-rack (~> 1.0)
39
+ faraday-retry (~> 1.0)
40
+ ruby2_keywords (>= 0.0.4)
41
+ faraday-em_http (1.0.0)
42
+ faraday-em_synchrony (1.0.0)
43
+ faraday-excon (1.1.0)
44
+ faraday-httpclient (1.0.1)
45
+ faraday-multipart (1.0.4)
46
+ multipart-post (~> 2)
47
+ faraday-net_http (1.0.1)
48
+ faraday-net_http_persistent (1.2.0)
49
+ faraday-patron (1.0.0)
50
+ faraday-rack (1.0.0)
51
+ faraday-retry (1.0.3)
52
+ ffi (1.17.4)
53
+ ffi-compiler (1.3.2)
54
+ ffi (>= 1.15.5)
55
+ rake
56
+ git (1.19.1)
57
+ addressable (~> 2.8)
58
+ rchardet (~> 1.8)
59
+ github_api (0.19.0)
60
+ addressable (~> 2.4)
61
+ descendants_tracker (~> 0.0.4)
62
+ faraday (>= 0.8, < 2)
63
+ hashie (~> 3.5, >= 3.5.2)
64
+ oauth2 (~> 1.0)
65
+ hashie (3.6.0)
66
+ highline (3.0.1)
67
+ http (5.2.0)
68
+ addressable (~> 2.8)
69
+ base64 (~> 0.1)
70
+ http-cookie (~> 1.0)
71
+ http-form_data (~> 2.2)
72
+ llhttp-ffi (~> 0.5.0)
73
+ http-cookie (1.0.5)
74
+ domain_name (~> 0.5)
75
+ http-form_data (2.3.0)
76
+ i18n (1.14.4)
77
+ concurrent-ruby (~> 1.0)
78
+ juwelier (2.4.9)
79
+ builder
80
+ bundler
81
+ git
82
+ github_api
83
+ highline
84
+ kamelcase (~> 0)
85
+ nokogiri
86
+ psych
87
+ rake
88
+ rdoc
89
+ semver2
90
+ jwt (2.8.1)
91
+ base64
92
+ kamelcase (0.0.2)
93
+ semver2 (~> 3)
94
+ ld-eventsource (2.2.2)
95
+ concurrent-ruby (~> 1.0)
96
+ http (>= 4.4.1, < 6.0.0)
97
+ llhttp-ffi (0.5.0)
98
+ ffi-compiler (~> 1.0)
99
+ rake (~> 13.0)
100
+ macaddr (1.7.2)
101
+ systemu (~> 2.6.5)
102
+ mini_portile2 (2.8.9)
103
+ minitest (5.22.3)
104
+ minitest-focus (1.4.0)
105
+ minitest (>= 4, < 6)
106
+ minitest-reporters (1.6.1)
107
+ ansi
108
+ builder
109
+ minitest (>= 5.0)
110
+ ruby-progressbar
111
+ multi_json (1.15.0)
112
+ multi_xml (0.6.0)
113
+ multipart-post (2.4.0)
114
+ mutex_m (0.2.0)
115
+ nokogiri (1.18.9)
116
+ mini_portile2 (~> 2.8.2)
117
+ racc (~> 1.4)
118
+ oauth2 (1.4.11)
119
+ faraday (>= 0.17.3, < 3.0)
120
+ jwt (>= 1.0, < 3.0)
121
+ multi_json (~> 1.3)
122
+ multi_xml (~> 0.5)
123
+ rack (>= 1.2, < 4)
124
+ psych (5.1.2)
125
+ stringio
126
+ public_suffix (5.0.4)
127
+ racc (1.8.1)
128
+ rack (3.1.18)
129
+ rake (13.1.0)
130
+ rchardet (1.8.0)
131
+ rdoc (6.6.3.1)
132
+ psych (>= 4.0.0)
133
+ ruby-progressbar (1.13.0)
134
+ ruby2_keywords (0.0.5)
135
+ semantic_logger (4.15.0)
136
+ concurrent-ruby (~> 1.0)
137
+ semver2 (3.4.2)
138
+ simplecov (0.22.0)
139
+ docile (~> 1.1)
140
+ simplecov-html (~> 0.11)
141
+ simplecov_json_formatter (~> 0.1)
142
+ simplecov-html (0.12.3)
143
+ simplecov_json_formatter (0.1.4)
144
+ stringio (3.1.0)
145
+ systemu (2.6.5)
146
+ thread_safe (0.3.6)
147
+ timecop (0.9.8)
148
+ tzinfo (2.0.6)
149
+ concurrent-ruby (~> 1.0)
150
+ uuid (2.3.9)
151
+ macaddr (~> 1.0)
152
+ webrick (1.8.2)
153
+
154
+ PLATFORMS
155
+ ruby
156
+
157
+ DEPENDENCIES
158
+ activesupport (>= 4)
159
+ allocation_stats
160
+ benchmark-ips
161
+ bundler
162
+ concurrent-ruby (~> 1.0, >= 1.0.5)
163
+ faraday
164
+ juwelier (~> 2.4.9)
165
+ ld-eventsource
166
+ minitest
167
+ minitest-focus
168
+ minitest-reporters
169
+ rdoc
170
+ semantic_logger (!= 4.16.0)
171
+ simplecov
172
+ timecop
173
+ uuid
174
+ webrick
175
+
176
+ BUNDLED WITH
177
+ 2.3.5
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2023 Prefab, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # quonfig
2
+
3
+ Ruby SDK for [Quonfig](https://quonfig.com) — Feature Flags, Live Config, and Dynamic Log Levels.
4
+
5
+ > **Note:** This SDK is pre-1.0 and the API is not yet stable.
6
+
7
+ ## Installation
8
+
9
+ Add the gem to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'quonfig'
13
+ ```
14
+
15
+ Or install directly:
16
+
17
+ ```bash
18
+ gem install quonfig
19
+ ```
20
+
21
+ ## Quickstart
22
+
23
+ ```ruby
24
+ require 'quonfig'
25
+
26
+ client = Quonfig::Client.new(sdk_key: ENV['QUONFIG_BACKEND_SDK_KEY'])
27
+
28
+ # Feature flags
29
+ if client.enabled?('new-dashboard')
30
+ # show new dashboard
31
+ end
32
+
33
+ # Typed config values
34
+ limit = client.get_int('rate-limit')
35
+ name = client.get_string('app.display-name')
36
+ regions = client.get_string_list('allowed-regions')
37
+
38
+ # Context-aware evaluation — pass a context hash as the last argument
39
+ value = client.get_string('homepage-hero', user: { key: 'user-123', country: 'US' })
40
+ ```
41
+
42
+ ## Context
43
+
44
+ Contexts are hashes grouped by scope (`user`, `team`, `device`, etc.). You can
45
+ attach a context in three ways:
46
+
47
+ ### 1. Per-call context
48
+
49
+ ```ruby
50
+ client.get_bool('beta-feature', user: { key: 'user-123', plan: 'pro' })
51
+ ```
52
+
53
+ ### 2. `in_context` block
54
+
55
+ Everything evaluated inside the block sees the supplied context. The block's
56
+ return value is returned from `in_context`.
57
+
58
+ ```ruby
59
+ result = client.in_context(user: { key: 'user-123', plan: 'pro' }) do |bound|
60
+ {
61
+ hero: bound.get_string('homepage-hero'),
62
+ limit: bound.get_int('rate-limit'),
63
+ beta?: bound.enabled?('beta-feature')
64
+ }
65
+ end
66
+ ```
67
+
68
+ ### 3. `with_context` — BoundClient for repeated lookups
69
+
70
+ `with_context` returns an immutable `BoundClient` that carries the context on
71
+ every call. Useful when you want to pass a context-bound handle down the stack.
72
+
73
+ ```ruby
74
+ bound = client.with_context(user: { key: 'user-123', plan: 'pro' })
75
+
76
+ bound.get_string('homepage-hero')
77
+ bound.enabled?('beta-feature')
78
+ bound.get_int('rate-limit')
79
+ ```
80
+
81
+ ## Datadir / offline mode
82
+
83
+ For tests, CI, or air-gapped environments, point the client at a local workspace
84
+ directory instead of the Quonfig API. In datadir mode the SDK loads JSON config
85
+ files from disk and performs no network I/O.
86
+
87
+ ```ruby
88
+ client = Quonfig::Client.new(
89
+ datadir: '/path/to/workspace',
90
+ environment: 'production'
91
+ )
92
+
93
+ client.get_bool('feature-x')
94
+ ```
95
+
96
+ You can also set `QUONFIG_DIR` in the environment and omit the `datadir:`
97
+ option; when `QUONFIG_DIR` is set the SDK switches to datadir mode
98
+ automatically. `environment` is required in datadir mode — it can be provided
99
+ via the option or via `QUONFIG_ENVIRONMENT`.
100
+
101
+ ```bash
102
+ export QUONFIG_DIR=/path/to/workspace
103
+ export QUONFIG_ENVIRONMENT=production
104
+ ```
105
+
106
+ ```ruby
107
+ client = Quonfig::Client.new # reads QUONFIG_DIR + QUONFIG_ENVIRONMENT
108
+ ```
109
+
110
+ ## Environment variables
111
+
112
+ | Variable | Purpose |
113
+ |-----------------------------|------------------------------------------------------------------------------------------|
114
+ | `QUONFIG_BACKEND_SDK_KEY` | SDK key used to authenticate against the Quonfig API. Used when `sdk_key:` is omitted. |
115
+ | `QUONFIG_DIR` | Path to a workspace directory. When set, the SDK runs in datadir/offline mode. |
116
+ | `QUONFIG_ENVIRONMENT` | Environment name (`production`, `staging`, `development`) evaluated in datadir mode. |
117
+ | `QUONFIG_TELEMETRY_URL` | Overrides the telemetry endpoint. Defaults to `https://telemetry.quonfig.com`. |
118
+
119
+ ## Constructor options
120
+
121
+ ```ruby
122
+ Quonfig::Client.new(
123
+ sdk_key: '...', # required unless QUONFIG_BACKEND_SDK_KEY is set
124
+ api_urls: ['https://primary.quonfig.com'],
125
+ telemetry_url: 'https://telemetry.quonfig.com',
126
+ enable_sse: true,
127
+ enable_polling: false,
128
+ poll_interval: 60,
129
+ init_timeout: 10,
130
+ on_no_default: :error,
131
+ global_context: {},
132
+ datadir: '/path/to/workspace',
133
+ environment: 'production'
134
+ )
135
+ ```
136
+
137
+ | Option | Type | Default | Description |
138
+ |-------------------|----------------------------|---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
139
+ | `sdk_key` | `String` | `ENV['QUONFIG_BACKEND_SDK_KEY']` | SDK key for API authentication. |
140
+ | `api_urls` | `Array<String>` | `['https://primary.quonfig.com']` | Ordered list of API base URLs to try. SSE stream URLs are derived by prepending `stream.` to each hostname. |
141
+ | `telemetry_url` | `String` | `https://telemetry.quonfig.com` (or `ENV['QUONFIG_TELEMETRY_URL']`) | Base URL for the telemetry service. |
142
+ | `enable_sse` | `Boolean` | `true` | Receive real-time updates over Server-Sent Events. |
143
+ | `enable_polling` | `Boolean` | `false` | Poll the API on an interval as a fallback. |
144
+ | `poll_interval` | `Integer` (seconds) | `60` | Polling interval when `enable_polling` is `true`. |
145
+ | `init_timeout` | `Integer` (seconds) | `10` | Maximum time to wait for the initial config load. |
146
+ | `on_no_default` | `Symbol` | `:error` | Behavior when a key has no value and no default: `:error`, `:warn`, or `:ignore`. |
147
+ | `global_context` | `Hash` | `{}` | Context applied to every evaluation. |
148
+ | `datadir` | `String` | `ENV['QUONFIG_DIR']` | Path to a local workspace. When set, the SDK runs offline from disk. |
149
+ | `environment` | `String` | `ENV['QUONFIG_ENVIRONMENT']` | Environment to evaluate in datadir mode. Required when `datadir` is set. |
150
+
151
+ ## Typed getters
152
+
153
+ Each typed getter takes a config key and an optional context hash. If the key
154
+ is missing or the stored value does not match the requested type, the getter
155
+ returns `nil`.
156
+
157
+ | Method | Returns |
158
+ |-------------------------------------------------|-------------------------------|
159
+ | `get_string(key, contexts = nil)` | `String` or `nil` |
160
+ | `get_int(key, contexts = nil)` | `Integer` or `nil` |
161
+ | `get_float(key, contexts = nil)` | `Float` or `nil` |
162
+ | `get_bool(key, contexts = nil)` | `true`, `false`, or `nil` |
163
+ | `get_string_list(key, contexts = nil)` | `Array<String>` or `nil` |
164
+ | `get_duration(key, contexts = nil)` | `Float` (seconds) or `nil` |
165
+ | `get_json(key, contexts = nil)` | `Hash`, `Array`, or `nil` |
166
+ | `enabled?(feature_name, contexts = nil)` | `true` or `false` |
167
+
168
+ Example:
169
+
170
+ ```ruby
171
+ client.get_string('app.display-name')
172
+ client.get_int('rate-limit', user: { key: 'user-123' })
173
+ client.get_float('pricing.multiplier')
174
+ client.get_bool('flags.new-checkout')
175
+ client.get_string_list('allowed-regions')
176
+ client.get_duration('request-timeout')
177
+ client.get_json('homepage.layout')
178
+ client.enabled?('beta-feature', user: { key: 'user-123' })
179
+ ```
180
+
181
+ ## Dynamic log levels (SemanticLogger)
182
+
183
+ Quonfig can drive per-class log levels at runtime. Set config keys like
184
+ `log-levels.my_app.foo.bar` to one of `trace`, `debug`, `info`, `warn`, `error`,
185
+ `fatal` and wire the filter into SemanticLogger:
186
+
187
+ ```ruby
188
+ require 'quonfig'
189
+ require 'semantic_logger'
190
+
191
+ client = Quonfig::Client.new(sdk_key: ENV['QUONFIG_BACKEND_SDK_KEY'])
192
+ SemanticLogger.add_appender(io: $stdout, filter: client.semantic_logger_filter)
193
+ ```
194
+
195
+ Lookup is exact-match only: logger name `MyApp::Foo::Bar` normalizes to
196
+ `log-levels.my_app.foo.bar`. If no key is set the log is allowed through and
197
+ SemanticLogger's static level decides. There is no hierarchy walk — a value on
198
+ `log-levels.my_app` does not affect `log-levels.my_app.foo.bar`.
199
+
200
+ Pass `key_prefix:` to use a prefix other than `log-levels.`:
201
+
202
+ ```ruby
203
+ client.semantic_logger_filter(key_prefix: 'debug.')
204
+ ```
205
+
206
+ ## Documentation
207
+
208
+ Full documentation, including SPEC, SDK reference, and operational guides, is
209
+ available at [https://quonfig.com/docs](https://quonfig.com/docs).
210
+
211
+ ## License
212
+
213
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ warn e.message
9
+ warn 'Run `bundle install` to install missing gems'
10
+ exit e.status_code
11
+ end
12
+
13
+ require 'rake'
14
+
15
+ require 'rake/testtask'
16
+ Rake::TestTask.new(:test) do |test|
17
+ test.libs << 'lib' << 'test'
18
+ test.pattern = 'test/**/test_*.rb'
19
+ test.verbose = true
20
+ end
21
+
22
+ task default: :test
23
+
24
+ unless ENV['CI']
25
+ require 'juwelier'
26
+ Juwelier::Tasks.new do |gem|
27
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
28
+ gem.name = 'quonfig'
29
+ gem.homepage = 'https://github.com/quonfig/sdk-ruby'
30
+ gem.license = 'MIT'
31
+ gem.summary = %(Quonfig Ruby SDK)
32
+ gem.description = %(Quonfig — feature flags and live config, stored as files in git.)
33
+ gem.email = 'jeff@quonfig.com'
34
+ gem.authors = ['Jeff Dwyer']
35
+
36
+ # dependencies defined in Gemfile
37
+ end
38
+ Juwelier::RubygemsDotOrgTasks.new
39
+
40
+ desc 'Code coverage detail'
41
+ task :simplecov do
42
+ ENV['COVERAGE'] = 'true'
43
+ Rake::Task['test'].execute
44
+ end
45
+
46
+ require 'rdoc/task'
47
+ Rake::RDocTask.new do |rdoc|
48
+ version = File.exist?('VERSION') ? File.read('VERSION') : ''
49
+
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = "quonfig #{version}"
52
+ rdoc.rdoc_files.include('README*')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
55
+ end
56
+
57
+ # Add release task for CI
58
+ task :release do
59
+ sh 'mkdir -p pkg'
60
+ version = File.read('VERSION').strip
61
+ gem_file = "pkg/quonfig-#{version}.gem"
62
+ sh "gem build quonfig.gemspec --output #{gem_file}"
63
+ sh "gem push #{gem_file}"
64
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+
6
+ gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
7
+ spec = Gem::Specification.load(gemspec)
8
+
9
+ # Add the require paths to the $LOAD_PATH
10
+ spec.require_paths.each do |path|
11
+ full_path = File.expand_path("../" + path, __dir__)
12
+ $LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
13
+ end
14
+
15
+ spec.require_paths.each do |path|
16
+ require "./lib/reforge-sdk"
17
+ end
18
+
19
+ require 'reforge-sdk'
20
+
21
+ $prefab = Reforge::Client.new(collect_logger_counts: false, collect_evaluation_summaries: false,
22
+ context_upload_mode: :none)
23
+ $prefab.get('a.live.integer')
24
+
25
+ puts '-' * 80
26
+
27
+ require 'allocation_stats'
28
+
29
+ $runs = 100
30
+
31
+ def measure(description)
32
+ puts "Measuring #{description}..."
33
+ stats = $prefab.with_context(user: { email_suffix: 'yahoo.com' }) do
34
+ AllocationStats.trace do
35
+ $runs.times do
36
+ yield
37
+ end
38
+ end
39
+ end
40
+
41
+ allocations = stats.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class)
42
+
43
+ if ENV['TOP']
44
+ puts allocations.sort_by_size.to_text.split("\n").first(20)
45
+ end
46
+
47
+ puts "Total allocations: #{allocations.all.values.map(&:size).sum}"
48
+ puts "Total memory: #{allocations.all.values.flatten.map(&:memsize).sum}"
49
+ puts stats.gc_profiler_report
50
+ end
51
+
52
+ measure "no-JIT context (#{$runs} runs)" do
53
+ $prefab.get('a.live.integer')
54
+ end
55
+
56
+ puts "\n\n"
57
+
58
+ measure "with JIT context (#{$runs} runs)" do
59
+ $prefab.get('a.live.integer', { a: { b: "c" } })
60
+ end
data/dev/benchmark ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+
6
+ gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
7
+ spec = Gem::Specification.load(gemspec)
8
+
9
+ # Add the require paths to the $LOAD_PATH
10
+ spec.require_paths.each do |path|
11
+ full_path = File.expand_path("../" + path, __dir__)
12
+ $LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
13
+ end
14
+
15
+ spec.require_paths.each do |path|
16
+ require "./lib/reforge-sdk"
17
+ end
18
+
19
+ require 'reforge-sdk'
20
+ require 'benchmark/ips'
21
+
22
+ prefab = Reforge::Client.new(collect_logger_counts: false, collect_evaluation_summaries: false,
23
+ context_upload_mode: :none)
24
+
25
+ prefab.get('prefab.auth.allowed_origins')
26
+
27
+ prefab.with_context(user: { email_suffix: 'yahoo.com' }) do
28
+ Benchmark.ips do |x|
29
+ x.report("noop") do
30
+ end
31
+
32
+ x.report('prefab.get') do
33
+ prefab.get('prefab.auth.allowed_origins')
34
+ end
35
+
36
+ x.report('prefab.get with jit context') do
37
+ prefab.get('prefab.auth.allowed_origins', { a: { b: "c" } })
38
+ end
39
+ end
40
+ end
data/dev/console ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bundle exec ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'irb'
5
+ require_relative "./script_setup"
6
+
7
+ if !ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL']
8
+ puts "run with REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL=debug (or trace) for more output"
9
+ end
10
+
11
+ # Start an IRB session
12
+ IRB.start(__FILE__)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
6
+ spec = Gem::Specification.load(gemspec)
7
+
8
+ # Add the require paths to the $LOAD_PATH
9
+ spec.require_paths.each do |path|
10
+ full_path = File.expand_path("../" + path, __dir__)
11
+ $LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
12
+ end
13
+
14
+ spec.require_paths.each do |path|
15
+ require "./lib/quonfig"
16
+ end
17
+
18
+ SemanticLogger.add_appender(io: $stdout)