quonfig 0.0.3 → 0.0.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/VERSION +1 -1
- data/lib/quonfig/client.rb +196 -15
- data/lib/quonfig/config_loader.rb +75 -13
- data/lib/quonfig/config_store.rb +4 -0
- data/lib/quonfig/options.rb +7 -1
- data/lib/quonfig/semantic_logger_filter.rb +20 -22
- data/lib/quonfig/sse_config_client.rb +23 -2
- data/quonfig.gemspec +1 -0
- data/test/test_client_network_mode.rb +136 -0
- data/test/test_semantic_logger_filter.rb +37 -16
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb6a88a2132fd0fe54fc656ffe1b81916e4487258c2a54dd5589d0bb56b1e300
|
|
4
|
+
data.tar.gz: e91e3cce0cc73793e2c0c1fb407192cbd5a8caad9c96bfbfae6d6bf2fa69ea78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d33195dcf4fd9b52f8e245e102fbf21a405626aaa8e1157294a79c83cb8a1f91b78249782a9b7318728f31d342adc9f4565d082886d041d53a44995f8adfcb45
|
|
7
|
+
data.tar.gz: fa6b7dd2497f9d202e985619ccccf754162b538c1cd7ffc41d0c7f6679e83d55ac1739d3ed1dc76931b7a9a14be1db9e5f35514a9ff86c9997feecda9342159d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.5 - 2026-04-22
|
|
4
|
+
|
|
5
|
+
- **BREAKING — SemanticLoggerFilter context key renamed.** The filter
|
|
6
|
+
previously exposed the logger name under
|
|
7
|
+
`{ 'quonfig' => { 'logger-name' => '<normalized>' } }`. It now uses
|
|
8
|
+
`{ 'quonfig-sdk-logging' => { 'key' => '<verbatim name>' } }` so that
|
|
9
|
+
all SDKs (node, go, ruby, python) share one top-level context name.
|
|
10
|
+
Any customer rules that match on the old `quonfig.logger-name` property
|
|
11
|
+
must be rewritten to match `quonfig-sdk-logging.key`.
|
|
12
|
+
- **BREAKING — logger name normalization removed.** The filter no longer
|
|
13
|
+
converts `MyApp::Services::Auth` → `my_app.services.auth`. Native Ruby
|
|
14
|
+
class names are passed through verbatim. Rules should target the exact
|
|
15
|
+
class name (e.g. `PROP_STARTS_WITH_ONE_OF "MyApp::Services::"`).
|
|
16
|
+
- **New: `logger_key` client option** (snake_case) — pass to
|
|
17
|
+
`Quonfig::Options.new(logger_key: 'log-level.my-app')` or via
|
|
18
|
+
`Quonfig.init`. Declares the Quonfig config key the higher-level
|
|
19
|
+
`should_log?` helper evaluates for every log call.
|
|
20
|
+
- **New: `client.should_log?(logger_path:, desired_level:, contexts:)`** —
|
|
21
|
+
Reforge-style convenience on top of `get`. Evaluates `logger_key` with
|
|
22
|
+
`{ 'quonfig-sdk-logging' => { 'key' => logger_path } }` merged into the
|
|
23
|
+
caller's contexts, then compares the returned level to `desired_level`.
|
|
24
|
+
Raises `Quonfig::Error` if `logger_key` was not set at init. Parallels
|
|
25
|
+
sdk-node's `shouldLog({loggerPath})` and sdk-go's `ShouldLogPath`.
|
|
26
|
+
- Stage 1 of the per-SDK logger-path rollout (after sdk-node 0.0.14 and
|
|
27
|
+
sdk-go 0.0.10 shipped the same shape).
|
|
28
|
+
|
|
29
|
+
## 0.0.4 - 2026-04-22
|
|
30
|
+
|
|
31
|
+
- **Fix (P0 from test-ruby friction log):** Network mode is now wired through
|
|
32
|
+
`Client`. Previously, `Quonfig.init` with just `QUONFIG_BACKEND_SDK_KEY`
|
|
33
|
+
succeeded silently against an empty store; `get` and `enabled?` returned
|
|
34
|
+
the default for every key because no HTTP fetch ever happened. Now:
|
|
35
|
+
- On `Client#initialize` (when neither `datadir:` nor `store:` is passed)
|
|
36
|
+
we do a synchronous HTTP GET against the first `api_urls[0]` (failing
|
|
37
|
+
over to secondaries), bounded by `initialization_timeout_sec` (default
|
|
38
|
+
10s). `on_init_failure` decides raise vs continue with empty store.
|
|
39
|
+
- `enable_sse` (default `true`) subscribes to `{stream.*}/api/v2/sse/config`
|
|
40
|
+
and applies incremental envelopes to the live `ConfigStore`.
|
|
41
|
+
- `enable_polling` (default `true`) starts a background poller IFF SSE did
|
|
42
|
+
not start successfully. This avoids double-fetching when SSE is healthy
|
|
43
|
+
while still refreshing in proxied / SSE-blocked environments. Interval
|
|
44
|
+
comes from `Options#poll_interval` (default 60s).
|
|
45
|
+
- `Client#stop` now closes the SSE connection and kills the poll thread.
|
|
46
|
+
- Adds `Options#poll_interval` (default 60s); previously missing from the
|
|
47
|
+
Options surface despite being documented.
|
|
48
|
+
- `ConfigLoader` now populates the `ConfigStore` directly on each successful
|
|
49
|
+
fetch, so the Evaluator/Resolver see the new configs immediately (wire
|
|
50
|
+
path matches sdk-node/sdk-go — `ConfigResponse` envelope JSON). (qfg-s7h)
|
|
51
|
+
|
|
3
52
|
## 0.0.3 - 2026-04-22
|
|
4
53
|
|
|
5
54
|
- **Release plumbing only** — no functional changes. Renames the release
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.5
|
data/lib/quonfig/client.rb
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'timeout'
|
|
4
5
|
|
|
5
6
|
module Quonfig
|
|
6
7
|
# Public Quonfig SDK client.
|
|
7
8
|
#
|
|
8
|
-
# Wires the
|
|
9
|
-
# Quonfig::Resolver.
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# +
|
|
13
|
-
#
|
|
9
|
+
# Wires the JSON stack: Quonfig::ConfigStore + Quonfig::Evaluator +
|
|
10
|
+
# Quonfig::Resolver. Three modes are supported:
|
|
11
|
+
#
|
|
12
|
+
# 1. +datadir:+ (offline) -- load a workspace from the local filesystem.
|
|
13
|
+
# 2. +store:+ (test harness) -- caller-supplied ConfigStore, no I/O.
|
|
14
|
+
# 3. network mode (default) -- HTTP fetch from +api_urls+ populates the
|
|
15
|
+
# ConfigStore, then (if enabled) an SSE subscription keeps it live.
|
|
16
|
+
#
|
|
17
|
+
# Network mode is the happy path for production SDK usage. The protobuf
|
|
18
|
+
# stack was retired in qfg-dk6.32; HTTP + SSE were wired back through Client
|
|
19
|
+
# in qfg-s7h.
|
|
14
20
|
class Client
|
|
15
21
|
LOG = Quonfig::InternalLogger.new(self)
|
|
16
22
|
|
|
17
|
-
attr_reader :options, :resolver, :store, :evaluator, :instance_hash
|
|
23
|
+
attr_reader :options, :resolver, :store, :evaluator, :instance_hash,
|
|
24
|
+
:config_loader
|
|
18
25
|
|
|
19
26
|
def initialize(options = nil, store: nil, **option_kwargs)
|
|
20
27
|
@options =
|
|
@@ -27,10 +34,22 @@ module Quonfig
|
|
|
27
34
|
end
|
|
28
35
|
@global_context = normalize_context(@options.global_context)
|
|
29
36
|
@instance_hash = SecureRandom.uuid
|
|
30
|
-
@store = store ||
|
|
37
|
+
@store = store || Quonfig::ConfigStore.new
|
|
31
38
|
@evaluator = Quonfig::Evaluator.new(@store, env_id: @options.environment)
|
|
32
39
|
@resolver = Quonfig::Resolver.new(@store, @evaluator)
|
|
33
40
|
@semantic_logger_filters = {}
|
|
41
|
+
@sse_client = nil
|
|
42
|
+
@poll_thread = nil
|
|
43
|
+
@stopped = false
|
|
44
|
+
|
|
45
|
+
# If the caller injected a store, we're in test/bootstrap mode; skip I/O.
|
|
46
|
+
return if store
|
|
47
|
+
|
|
48
|
+
if @options.datadir
|
|
49
|
+
load_datadir_into_store
|
|
50
|
+
else
|
|
51
|
+
initialize_network_mode
|
|
52
|
+
end
|
|
34
53
|
end
|
|
35
54
|
|
|
36
55
|
# ---- Lookup --------------------------------------------------------
|
|
@@ -106,13 +125,81 @@ module Quonfig
|
|
|
106
125
|
Quonfig::SemanticLoggerFilter.new(self, config_key: config_key)
|
|
107
126
|
end
|
|
108
127
|
|
|
128
|
+
# The configured +logger_key+ from Options — the Quonfig config key the
|
|
129
|
+
# higher-level +should_log?+ helper evaluates per-logger. +nil+ if the
|
|
130
|
+
# client was not configured for dynamic log levels.
|
|
131
|
+
def logger_key
|
|
132
|
+
@options.logger_key
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Higher-level log-level check — a convenience on top of the primitive
|
|
136
|
+
# +get+. Evaluates the client's +logger_key+ config and returns whether
|
|
137
|
+
# a message at +desired_level+ should be emitted for +logger_path+.
|
|
138
|
+
#
|
|
139
|
+
# The SDK injects +logger_path+ under the +quonfig-sdk-logging+ named
|
|
140
|
+
# context with property +key+ so a single log-level config can drive
|
|
141
|
+
# per-logger overrides via the normal rule engine (e.g.
|
|
142
|
+
# PROP_STARTS_WITH_ONE_OF "MyApp::Services::").
|
|
143
|
+
#
|
|
144
|
+
# +logger_path+ is passed through verbatim — the SDK does not normalize
|
|
145
|
+
# it. Callers may pass any identifier shape their host language prefers
|
|
146
|
+
# (dotted, colon, slash, etc.) and author matching rules in the config
|
|
147
|
+
# against that exact shape.
|
|
148
|
+
#
|
|
149
|
+
# Parallels sdk-node's +shouldLog({loggerPath})+ and sdk-go's
|
|
150
|
+
# +ShouldLogPath+.
|
|
151
|
+
#
|
|
152
|
+
# Raises +Quonfig::Error+ if +logger_key+ was not set on the client —
|
|
153
|
+
# use +semantic_logger_filter(config_key:)+ directly if you want to
|
|
154
|
+
# evaluate a specific key without declaring it at init time.
|
|
155
|
+
#
|
|
156
|
+
# @param logger_path [String] native logger name (typically a class name).
|
|
157
|
+
# @param desired_level [Symbol, String] the level the caller wants to
|
|
158
|
+
# emit at (:trace, :debug, :info, :warn, :error, :fatal).
|
|
159
|
+
# @param contexts [Hash] optional extra context to merge with the
|
|
160
|
+
# injected logger context.
|
|
161
|
+
# @return [Boolean] true if the message should be emitted.
|
|
162
|
+
def should_log?(logger_path:, desired_level:, contexts: {})
|
|
163
|
+
unless logger_key
|
|
164
|
+
raise Quonfig::Error,
|
|
165
|
+
'logger_key must be set at init to use should_log?(logger_path:, ...). ' \
|
|
166
|
+
'Pass `logger_key:` to Quonfig::Options.new, or call ' \
|
|
167
|
+
'semantic_logger_filter(config_key:) / get(config_key) directly.'
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
logger_context = {
|
|
171
|
+
Quonfig::SemanticLoggerFilter::LOGGER_CONTEXT_NAME => {
|
|
172
|
+
Quonfig::SemanticLoggerFilter::LOGGER_CONTEXT_KEY_PROP => logger_path
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
merged = merge_contexts(normalize_context(contexts), logger_context)
|
|
176
|
+
|
|
177
|
+
configured = get(logger_key, nil, merged)
|
|
178
|
+
return true if configured.nil?
|
|
179
|
+
|
|
180
|
+
desired_severity = Quonfig::SemanticLoggerFilter::LEVELS[normalize_log_level(desired_level)] ||
|
|
181
|
+
Quonfig::SemanticLoggerFilter::LEVELS[:debug]
|
|
182
|
+
min_severity = Quonfig::SemanticLoggerFilter::LEVELS[normalize_log_level(configured)] ||
|
|
183
|
+
Quonfig::SemanticLoggerFilter::LEVELS[:debug]
|
|
184
|
+
desired_severity >= min_severity
|
|
185
|
+
end
|
|
186
|
+
|
|
109
187
|
def on_update(&block)
|
|
110
188
|
@on_update = block
|
|
111
189
|
end
|
|
112
190
|
|
|
113
191
|
def stop
|
|
114
|
-
|
|
115
|
-
|
|
192
|
+
@stopped = true
|
|
193
|
+
begin
|
|
194
|
+
@sse_client&.close
|
|
195
|
+
rescue StandardError => e
|
|
196
|
+
LOG.debug "Error closing SSE client: #{e.message}"
|
|
197
|
+
end
|
|
198
|
+
@sse_client = nil
|
|
199
|
+
|
|
200
|
+
thread = @poll_thread
|
|
201
|
+
@poll_thread = nil
|
|
202
|
+
thread&.kill
|
|
116
203
|
end
|
|
117
204
|
|
|
118
205
|
def fork
|
|
@@ -125,11 +212,97 @@ module Quonfig
|
|
|
125
212
|
|
|
126
213
|
private
|
|
127
214
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
215
|
+
def load_datadir_into_store
|
|
216
|
+
envelope = Quonfig::Datadir.load_envelope(@options.datadir, @options.environment)
|
|
217
|
+
envelope.configs.each { |cfg| @store.set(cfg['key'], cfg) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Initialize network mode: sync HTTP fetch (bounded by
|
|
221
|
+
# initialization_timeout_sec) then start SSE + polling as requested.
|
|
222
|
+
def initialize_network_mode
|
|
223
|
+
if @options.sdk_key.nil? || @options.sdk_key.to_s.strip.empty?
|
|
224
|
+
raise Quonfig::Errors::InvalidSdkKeyError, @options.sdk_key
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
@config_loader = Quonfig::ConfigLoader.new(@store, @options)
|
|
228
|
+
|
|
229
|
+
perform_initial_fetch
|
|
230
|
+
|
|
231
|
+
sse_started = @options.enable_sse && start_sse
|
|
232
|
+
|
|
233
|
+
# Polling is a fallback: if SSE is off or failed to start, poll. This
|
|
234
|
+
# avoids double-work when SSE is healthy but still refreshes the store
|
|
235
|
+
# in environments that block SSE (corporate proxies, Lambda, etc.).
|
|
236
|
+
start_polling if @options.enable_polling && !sse_started
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def perform_initial_fetch
|
|
240
|
+
timeout = @options.initialization_timeout_sec || 10
|
|
241
|
+
result = :failed
|
|
242
|
+
|
|
243
|
+
begin
|
|
244
|
+
Timeout.timeout(timeout) do
|
|
245
|
+
result = @config_loader.fetch!
|
|
246
|
+
end
|
|
247
|
+
rescue Timeout::Error
|
|
248
|
+
handle_init_failure(
|
|
249
|
+
Quonfig::Errors::InitializationTimeoutError.new(timeout, nil)
|
|
250
|
+
)
|
|
251
|
+
return
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
handle_init_failure(RuntimeError.new('Config fetch failed against all api_urls')) if result == :failed
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def handle_init_failure(err)
|
|
258
|
+
if @options.on_init_failure == Quonfig::Options::ON_INITIALIZATION_FAILURE::RETURN
|
|
259
|
+
LOG.warn "[quonfig] Initialization did not complete cleanly; continuing with empty store: #{err.message}"
|
|
260
|
+
return
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
raise err
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Returns true if SSE started successfully, false otherwise. A false here
|
|
267
|
+
# signals the caller to fall back to polling.
|
|
268
|
+
def start_sse
|
|
269
|
+
return false if @options.sse_api_urls.nil? || @options.sse_api_urls.empty?
|
|
270
|
+
|
|
271
|
+
@sse_client = Quonfig::SSEConfigClient.new(@options, @config_loader)
|
|
272
|
+
@sse_client.start do |envelope, _event, _source|
|
|
273
|
+
next if @stopped
|
|
274
|
+
begin
|
|
275
|
+
@config_loader.apply_envelope(envelope)
|
|
276
|
+
@on_update&.call
|
|
277
|
+
rescue StandardError => e
|
|
278
|
+
LOG.warn "[quonfig] Error applying SSE envelope: #{e.message}"
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
true
|
|
282
|
+
rescue StandardError => e
|
|
283
|
+
LOG.warn "[quonfig] SSE start failed: #{e.message}"
|
|
284
|
+
@sse_client = nil
|
|
285
|
+
false
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def start_polling
|
|
289
|
+
poll_interval = @options.respond_to?(:poll_interval) && @options.poll_interval ? @options.poll_interval : 60
|
|
290
|
+
return if poll_interval <= 0
|
|
291
|
+
|
|
292
|
+
@poll_thread = Thread.new do
|
|
293
|
+
Thread.current.name = 'quonfig-poller'
|
|
294
|
+
loop do
|
|
295
|
+
break if @stopped
|
|
296
|
+
sleep poll_interval
|
|
297
|
+
break if @stopped
|
|
298
|
+
|
|
299
|
+
begin
|
|
300
|
+
@config_loader.fetch!
|
|
301
|
+
@on_update&.call
|
|
302
|
+
rescue StandardError => e
|
|
303
|
+
LOG.warn "[quonfig] Polling error: #{e.message}"
|
|
304
|
+
end
|
|
305
|
+
end
|
|
133
306
|
end
|
|
134
307
|
end
|
|
135
308
|
|
|
@@ -164,6 +337,14 @@ module Quonfig
|
|
|
164
337
|
merged
|
|
165
338
|
end
|
|
166
339
|
|
|
340
|
+
def normalize_log_level(level)
|
|
341
|
+
case level
|
|
342
|
+
when Symbol then level.downcase
|
|
343
|
+
when String then level.downcase.to_sym
|
|
344
|
+
else level
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
167
348
|
def handle_missing(key, default)
|
|
168
349
|
return default if default != NO_DEFAULT_PROVIDED
|
|
169
350
|
|
|
@@ -3,26 +3,54 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
module Quonfig
|
|
6
|
+
# Fetches config envelopes from the Quonfig delivery API and installs them
|
|
7
|
+
# into a ConfigStore.
|
|
8
|
+
#
|
|
9
|
+
# Wire format matches sdk-node's Transport + ConfigStore:
|
|
10
|
+
# GET /api/v2/configs
|
|
11
|
+
# -> 200 { "configs": [...], "meta": { "version": "...", "environment": "..." } }
|
|
12
|
+
# -> 304 Not Modified (ETag honored via If-None-Match)
|
|
13
|
+
#
|
|
14
|
+
# The fetch is synchronous; Client is responsible for timing out the initial
|
|
15
|
+
# fetch per `initialization_timeout_sec`.
|
|
6
16
|
class ConfigLoader
|
|
7
17
|
LOG = Quonfig::InternalLogger.new(self)
|
|
8
18
|
|
|
9
19
|
CONFIGS_PATH = '/api/v2/configs'
|
|
10
20
|
|
|
11
|
-
attr_reader :etag
|
|
21
|
+
attr_reader :etag, :version, :environment_id
|
|
22
|
+
|
|
23
|
+
# +store+: the Quonfig::ConfigStore to populate on successful fetch.
|
|
24
|
+
# +options+: a Quonfig::Options instance (supplies sdk_key + config_api_urls).
|
|
25
|
+
# +logger+: optional logger override (defaults to module LOG).
|
|
26
|
+
#
|
|
27
|
+
# Backward compat: callers that pass a single +base_client+ (mock client
|
|
28
|
+
# used by tests that expects `.options`) are still supported.
|
|
29
|
+
def initialize(store_or_base_client, options = nil, logger: nil)
|
|
30
|
+
if options.nil? && store_or_base_client.respond_to?(:options)
|
|
31
|
+
# Legacy shape: ConfigLoader.new(base_client)
|
|
32
|
+
@options = store_or_base_client.options
|
|
33
|
+
@store = nil
|
|
34
|
+
else
|
|
35
|
+
@store = store_or_base_client
|
|
36
|
+
@options = options
|
|
37
|
+
end
|
|
12
38
|
|
|
13
|
-
def initialize(base_client)
|
|
14
|
-
@base_client = base_client
|
|
15
|
-
@options = base_client.options
|
|
16
39
|
@api_config = Concurrent::Map.new
|
|
17
40
|
@etag = nil
|
|
41
|
+
@version = nil
|
|
42
|
+
@environment_id = nil
|
|
43
|
+
@logger = logger || LOG
|
|
18
44
|
end
|
|
19
45
|
|
|
20
46
|
# Fetch configs from /api/v2/configs with ETag / If-None-Match caching.
|
|
47
|
+
# On 200 responses, installs the envelope into the attached ConfigStore
|
|
48
|
+
# (if one was provided).
|
|
21
49
|
#
|
|
22
50
|
# Returns one of:
|
|
23
|
-
# :updated
|
|
24
|
-
# :not_modified
|
|
25
|
-
# :failed
|
|
51
|
+
# :updated -- 200 response; store replaced
|
|
52
|
+
# :not_modified -- 304 response; store untouched
|
|
53
|
+
# :failed -- every configured source failed
|
|
26
54
|
def fetch!
|
|
27
55
|
Array(@options.config_api_urls).each do |api_url|
|
|
28
56
|
result = fetch_from(api_url)
|
|
@@ -31,6 +59,12 @@ module Quonfig
|
|
|
31
59
|
:failed
|
|
32
60
|
end
|
|
33
61
|
|
|
62
|
+
# Apply a ConfigEnvelope (from SSE) to the store. Called by the SSE client
|
|
63
|
+
# when a new event arrives.
|
|
64
|
+
def apply_envelope(envelope)
|
|
65
|
+
install_envelope(envelope, source: :sse)
|
|
66
|
+
end
|
|
67
|
+
|
|
34
68
|
def calc_config
|
|
35
69
|
rtn = {}
|
|
36
70
|
@api_config.each_key do |k|
|
|
@@ -59,21 +93,24 @@ module Quonfig
|
|
|
59
93
|
when 200
|
|
60
94
|
new_etag = response.headers['ETag'] || response.headers['etag']
|
|
61
95
|
envelope = parse_envelope(response.body)
|
|
62
|
-
|
|
96
|
+
install_envelope(envelope, source: source)
|
|
63
97
|
@etag = new_etag
|
|
64
98
|
:updated
|
|
65
99
|
when 304
|
|
66
|
-
|
|
100
|
+
@logger.debug "Configs not modified (304) from #{source}"
|
|
67
101
|
:not_modified
|
|
102
|
+
when 401, 403
|
|
103
|
+
@logger.warn "Config fetch rejected (#{response.status}) from #{source}: #{short_body(response)}"
|
|
104
|
+
:failed
|
|
68
105
|
else
|
|
69
|
-
|
|
106
|
+
@logger.info "Config fetch failed: status #{response.status} from #{source}"
|
|
70
107
|
:failed
|
|
71
108
|
end
|
|
72
109
|
rescue Faraday::ConnectionFailed => e
|
|
73
|
-
|
|
110
|
+
@logger.debug "Connection failure fetching configs from #{source}: #{e.message}"
|
|
74
111
|
:failed
|
|
75
112
|
rescue StandardError => e
|
|
76
|
-
|
|
113
|
+
@logger.warn "Unexpected error fetching configs from #{source}: #{e.message}"
|
|
77
114
|
:failed
|
|
78
115
|
end
|
|
79
116
|
|
|
@@ -85,7 +122,14 @@ module Quonfig
|
|
|
85
122
|
)
|
|
86
123
|
end
|
|
87
124
|
|
|
88
|
-
def
|
|
125
|
+
def short_body(response)
|
|
126
|
+
return '' if response.body.nil?
|
|
127
|
+
str = response.body.to_s
|
|
128
|
+
str.length > 200 ? str[0, 200] + '...' : str
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def install_envelope(envelope, source:)
|
|
132
|
+
# Update internal tracking map (for legacy callers / introspection).
|
|
89
133
|
next_map = Concurrent::Map.new
|
|
90
134
|
envelope.configs.each do |cfg|
|
|
91
135
|
key = config_key(cfg)
|
|
@@ -93,6 +137,24 @@ module Quonfig
|
|
|
93
137
|
next_map[key] = { source: source, config: cfg }
|
|
94
138
|
end
|
|
95
139
|
@api_config = next_map
|
|
140
|
+
|
|
141
|
+
meta = envelope.meta || {}
|
|
142
|
+
@version = meta['version'] || meta[:version] || @version
|
|
143
|
+
@environment_id = meta['environment'] || meta[:environment] || @environment_id
|
|
144
|
+
|
|
145
|
+
# Replace the live store atomically.
|
|
146
|
+
return if @store.nil?
|
|
147
|
+
|
|
148
|
+
new_keys = next_map.keys.to_set
|
|
149
|
+
old_keys = @store.keys.to_set
|
|
150
|
+
# Drop keys that disappeared server-side.
|
|
151
|
+
(old_keys - new_keys).each { |k| @store.delete(k) } if @store.respond_to?(:delete)
|
|
152
|
+
|
|
153
|
+
envelope.configs.each do |cfg|
|
|
154
|
+
key = config_key(cfg)
|
|
155
|
+
next if key.nil?
|
|
156
|
+
@store.set(key, cfg)
|
|
157
|
+
end
|
|
96
158
|
end
|
|
97
159
|
|
|
98
160
|
def config_key(cfg)
|
data/lib/quonfig/config_store.rb
CHANGED
data/lib/quonfig/options.rb
CHANGED
|
@@ -18,7 +18,9 @@ module Quonfig
|
|
|
18
18
|
attr_reader :datadir
|
|
19
19
|
attr_reader :enable_sse
|
|
20
20
|
attr_reader :enable_polling
|
|
21
|
+
attr_reader :poll_interval
|
|
21
22
|
attr_reader :global_context
|
|
23
|
+
attr_reader :logger_key
|
|
22
24
|
attr_accessor :is_fork
|
|
23
25
|
|
|
24
26
|
module ON_INITIALIZATION_FAILURE
|
|
@@ -60,6 +62,7 @@ module Quonfig
|
|
|
60
62
|
datadir: ENV['QUONFIG_DIR'],
|
|
61
63
|
enable_sse: true,
|
|
62
64
|
enable_polling: true,
|
|
65
|
+
poll_interval: 60,
|
|
63
66
|
on_no_default: ON_NO_DEFAULT::RAISE,
|
|
64
67
|
initialization_timeout_sec: 10,
|
|
65
68
|
on_init_failure: ON_INITIALIZATION_FAILURE::RAISE,
|
|
@@ -70,13 +73,15 @@ module Quonfig
|
|
|
70
73
|
collect_evaluation_summaries: true,
|
|
71
74
|
collect_max_evaluation_summaries: DEFAULT_MAX_EVAL_SUMMARIES,
|
|
72
75
|
allow_telemetry_in_local_mode: false,
|
|
73
|
-
global_context: {}
|
|
76
|
+
global_context: {},
|
|
77
|
+
logger_key: nil
|
|
74
78
|
)
|
|
75
79
|
@sdk_key = sdk_key
|
|
76
80
|
@environment = environment
|
|
77
81
|
@datadir = datadir
|
|
78
82
|
@enable_sse = enable_sse
|
|
79
83
|
@enable_polling = enable_polling
|
|
84
|
+
@poll_interval = poll_interval
|
|
80
85
|
@on_no_default = on_no_default
|
|
81
86
|
@initialization_timeout_sec = initialization_timeout_sec
|
|
82
87
|
@on_init_failure = on_init_failure
|
|
@@ -88,6 +93,7 @@ module Quonfig
|
|
|
88
93
|
@allow_telemetry_in_local_mode = allow_telemetry_in_local_mode
|
|
89
94
|
@is_fork = false
|
|
90
95
|
@global_context = global_context
|
|
96
|
+
@logger_key = logger_key
|
|
91
97
|
|
|
92
98
|
# defaults that may be overridden by context_upload_mode
|
|
93
99
|
@collect_shapes = false
|
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module Quonfig
|
|
4
4
|
# SemanticLogger filter that gates log output by a single Quonfig config
|
|
5
|
-
# whose rules target the logger via the
|
|
6
|
-
# property.
|
|
5
|
+
# whose rules target the logger via the
|
|
6
|
+
# +quonfig-sdk-logging.key+ context property.
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
|
-
# filter = client.semantic_logger_filter(config_key: 'log-
|
|
9
|
+
# filter = client.semantic_logger_filter(config_key: 'log-level.my-app')
|
|
10
10
|
# SemanticLogger.add_appender(io: $stdout, filter: filter)
|
|
11
11
|
#
|
|
12
|
-
# The filter
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# discriminate per-logger via PROP_STARTS_WITH_ONE_OF /
|
|
16
|
-
#
|
|
12
|
+
# The filter exposes the SemanticLogger logger name (which is typically the
|
|
13
|
+
# native Ruby class name, e.g. +"MyApp::Services::Auth"+) under the
|
|
14
|
+
# +quonfig-sdk-logging+ named context with property +key+ so customer rules
|
|
15
|
+
# can discriminate per-logger via PROP_STARTS_WITH_ONE_OF /
|
|
16
|
+
# PROP_IS_ONE_OF etc. Lookup is O(1): one +client.get+ call per log line.
|
|
17
|
+
#
|
|
18
|
+
# Logger names are passed through verbatim — there is no snake_case
|
|
19
|
+
# normalization. Matching rules should target the exact class name
|
|
20
|
+
# (e.g. +MyApp::+, +MyApp::Services::Auth+).
|
|
21
|
+
#
|
|
22
|
+
# The constants +LOGGER_CONTEXT_NAME+ and +LOGGER_CONTEXT_KEY_PROP+ are
|
|
23
|
+
# load-bearing: they match +QUONFIG_SDK_LOGGING_CONTEXT_NAME+ in sdk-node
|
|
24
|
+
# and sdk-go, and are consumed by api-telemetry's example-context
|
|
25
|
+
# auto-capture. Do not rename in isolation.
|
|
17
26
|
class SemanticLoggerFilter
|
|
18
27
|
LEVELS = {
|
|
19
28
|
trace: 0,
|
|
@@ -24,7 +33,8 @@ module Quonfig
|
|
|
24
33
|
fatal: 5
|
|
25
34
|
}.freeze
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
LOGGER_CONTEXT_NAME = 'quonfig-sdk-logging'
|
|
37
|
+
LOGGER_CONTEXT_KEY_PROP = 'key'
|
|
28
38
|
|
|
29
39
|
def self.semantic_logger_loaded?
|
|
30
40
|
defined?(SemanticLogger)
|
|
@@ -50,22 +60,10 @@ module Quonfig
|
|
|
50
60
|
log_severity >= min_severity
|
|
51
61
|
end
|
|
52
62
|
|
|
53
|
-
# Normalize a SemanticLogger logger name to the dotted snake_case form
|
|
54
|
-
# the customer writes targeting rules against.
|
|
55
|
-
# MyApp::Foo::Bar → my_app.foo.bar
|
|
56
|
-
# HTMLParser → html_parser
|
|
57
|
-
def normalize(name)
|
|
58
|
-
name.to_s
|
|
59
|
-
.gsub('::', '.')
|
|
60
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
61
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
62
|
-
.downcase
|
|
63
|
-
end
|
|
64
|
-
|
|
65
63
|
private
|
|
66
64
|
|
|
67
65
|
def context_for(log)
|
|
68
|
-
{
|
|
66
|
+
{ LOGGER_CONTEXT_NAME => { LOGGER_CONTEXT_KEY_PROP => log.name.to_s } }
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
def normalize_level(level)
|
|
@@ -66,13 +66,14 @@ module Quonfig
|
|
|
66
66
|
|
|
67
67
|
def connect(&load_configs)
|
|
68
68
|
url = "#{source}/api/v2/sse/config"
|
|
69
|
-
|
|
69
|
+
cursor = current_cursor
|
|
70
|
+
@logger.debug "SSE Streaming Connect to #{url} start_at #{cursor.inspect}"
|
|
70
71
|
|
|
71
72
|
SSE::Client.new(url,
|
|
72
73
|
headers: headers,
|
|
73
74
|
read_timeout: @options.sse_read_timeout,
|
|
74
75
|
reconnect_time: @options.sse_default_reconnect_time,
|
|
75
|
-
last_event_id:
|
|
76
|
+
last_event_id: cursor,
|
|
76
77
|
logger: Quonfig::InternalLogger.new(SSE::Client)) do |client|
|
|
77
78
|
client.on_event do |event|
|
|
78
79
|
if event.data.nil? || event.data.empty?
|
|
@@ -131,5 +132,25 @@ module Quonfig
|
|
|
131
132
|
|
|
132
133
|
@prefab_options.sse_api_urls[@source_index]
|
|
133
134
|
end
|
|
135
|
+
|
|
136
|
+
# Compute a Last-Event-ID to resume the stream from. Three sources, in
|
|
137
|
+
# priority order:
|
|
138
|
+
# 1. config_loader.version -- string ETag from last HTTP fetch (new path)
|
|
139
|
+
# 2. config_loader.highwater_mark -- legacy numeric cursor
|
|
140
|
+
# 3. nil -- no prior state; stream from HEAD
|
|
141
|
+
def current_cursor
|
|
142
|
+
if @config_loader.respond_to?(:version)
|
|
143
|
+
v = @config_loader.version
|
|
144
|
+
return v if v.is_a?(String) && !v.empty?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if @config_loader.respond_to?(:highwater_mark)
|
|
148
|
+
hw = @config_loader.highwater_mark
|
|
149
|
+
return hw.to_s if hw.is_a?(Numeric) && hw.positive?
|
|
150
|
+
return hw if hw.is_a?(String) && !hw.empty?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
134
155
|
end
|
|
135
156
|
end
|
data/quonfig.gemspec
CHANGED
|
@@ -104,6 +104,7 @@ Gem::Specification.new do |s|
|
|
|
104
104
|
"test/test_bound_client.rb",
|
|
105
105
|
"test/test_caching_http_connection.rb",
|
|
106
106
|
"test/test_client.rb",
|
|
107
|
+
"test/test_client_network_mode.rb",
|
|
107
108
|
"test/test_config_loader.rb",
|
|
108
109
|
"test/test_context.rb",
|
|
109
110
|
"test/test_datadir.rb",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'webrick'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
# Verifies Client#initialize (qfg-s7h) wires HTTP fetch + ConfigStore together
|
|
8
|
+
# so `Quonfig.get(...)` / `Quonfig.enabled?(...)` return real values — not the
|
|
9
|
+
# defaults — when only an `sdk_key:` + `api_urls:` are supplied. This is the
|
|
10
|
+
# regression test for the P0 documented in test-ruby/FRICTION.md where
|
|
11
|
+
# network-mode was accepted but silently ignored in v0.0.3.
|
|
12
|
+
class TestClientNetworkMode < Minitest::Test
|
|
13
|
+
PORT = 18_094
|
|
14
|
+
|
|
15
|
+
SAMPLE_CONFIG = {
|
|
16
|
+
'id' => 'c1',
|
|
17
|
+
'key' => 'log-levels.test-ruby',
|
|
18
|
+
'type' => 'log_level',
|
|
19
|
+
'valueType' => 'log_level',
|
|
20
|
+
'sendToClientSdk' => false,
|
|
21
|
+
'default' => {
|
|
22
|
+
'rules' => [
|
|
23
|
+
{
|
|
24
|
+
'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
|
|
25
|
+
'value' => { 'type' => 'log_level', 'value' => 'WARN' }
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
def setup
|
|
32
|
+
super
|
|
33
|
+
@server = nil
|
|
34
|
+
@fetch_count = 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def teardown
|
|
38
|
+
@server&.shutdown
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def start_server
|
|
43
|
+
log = WEBrick::Log.new(StringIO.new)
|
|
44
|
+
@server = WEBrick::HTTPServer.new(
|
|
45
|
+
Port: PORT, Logger: log, AccessLog: []
|
|
46
|
+
)
|
|
47
|
+
@server.mount_proc '/api/v2/configs' do |_req, res|
|
|
48
|
+
@fetch_count += 1
|
|
49
|
+
res.status = 200
|
|
50
|
+
res['Content-Type'] = 'application/json'
|
|
51
|
+
res['ETag'] = "v#{@fetch_count}"
|
|
52
|
+
res.body = JSON.generate(
|
|
53
|
+
'configs' => [SAMPLE_CONFIG],
|
|
54
|
+
'meta' => { 'version' => "v#{@fetch_count}", 'environment' => 'dev' }
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
Thread.new { @server.start }
|
|
58
|
+
# Wait for server to be ready.
|
|
59
|
+
50.times do
|
|
60
|
+
break if tcp_open?
|
|
61
|
+
sleep 0.05
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def tcp_open?
|
|
66
|
+
require 'socket'
|
|
67
|
+
TCPSocket.new('127.0.0.1', PORT).tap(&:close)
|
|
68
|
+
true
|
|
69
|
+
rescue StandardError
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_initialize_fetches_configs_from_api_urls_and_populates_store
|
|
74
|
+
start_server
|
|
75
|
+
|
|
76
|
+
client = Quonfig::Client.new(
|
|
77
|
+
sdk_key: 'test-key',
|
|
78
|
+
api_urls: ["http://127.0.0.1:#{PORT}"],
|
|
79
|
+
enable_sse: false,
|
|
80
|
+
enable_polling: false
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
assert_equal 1, @fetch_count, 'expected exactly one HTTP fetch during init'
|
|
84
|
+
assert_includes client.keys, 'log-levels.test-ruby'
|
|
85
|
+
assert_equal 'WARN', client.get('log-levels.test-ruby', 'default')
|
|
86
|
+
ensure
|
|
87
|
+
client&.stop
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_initialize_raises_on_fetch_failure_by_default
|
|
91
|
+
# No server started -> connection refused everywhere
|
|
92
|
+
assert_raises(RuntimeError, Quonfig::Errors::InitializationTimeoutError) do
|
|
93
|
+
Quonfig::Client.new(
|
|
94
|
+
sdk_key: 'test-key',
|
|
95
|
+
api_urls: ['http://127.0.0.1:1'], # almost certainly unreachable
|
|
96
|
+
enable_sse: false,
|
|
97
|
+
enable_polling: false,
|
|
98
|
+
initialization_timeout_sec: 2
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_initialize_returns_empty_store_when_on_init_failure_is_return
|
|
104
|
+
client = Quonfig::Client.new(
|
|
105
|
+
sdk_key: 'test-key',
|
|
106
|
+
api_urls: ['http://127.0.0.1:1'],
|
|
107
|
+
enable_sse: false,
|
|
108
|
+
enable_polling: false,
|
|
109
|
+
initialization_timeout_sec: 2,
|
|
110
|
+
on_init_failure: Quonfig::Options::ON_INITIALIZATION_FAILURE::RETURN
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert_empty client.keys
|
|
114
|
+
assert_logged [/Initialization did not complete cleanly/]
|
|
115
|
+
ensure
|
|
116
|
+
client&.stop
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_initialize_skips_network_when_store_injected
|
|
120
|
+
# store: passed -> Client should not try any I/O. Unreachable URL must
|
|
121
|
+
# be fine when a store is injected.
|
|
122
|
+
store = Quonfig::ConfigStore.new
|
|
123
|
+
client = Quonfig::Client.new(
|
|
124
|
+
Quonfig::Options.new(
|
|
125
|
+
sdk_key: 'test-key',
|
|
126
|
+
api_urls: ['http://127.0.0.1:1'],
|
|
127
|
+
enable_sse: false,
|
|
128
|
+
enable_polling: false
|
|
129
|
+
),
|
|
130
|
+
store: store
|
|
131
|
+
)
|
|
132
|
+
assert_same store, client.store
|
|
133
|
+
ensure
|
|
134
|
+
client&.stop
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
require 'test_helper'
|
|
4
4
|
require 'semantic_logger'
|
|
5
5
|
|
|
6
|
-
# Verifies the
|
|
7
|
-
# The filter
|
|
8
|
-
#
|
|
6
|
+
# Verifies the SemanticLoggerFilter: ONE Quonfig config gates many loggers.
|
|
7
|
+
# The filter injects the native SemanticLogger logger name under the
|
|
8
|
+
# `quonfig-sdk-logging` context (keyed at `.key`) so customer rules can target
|
|
9
|
+
# `PROP_STARTS_WITH_ONE_OF MyApp::` etc.
|
|
10
|
+
#
|
|
11
|
+
# Breaking change in 0.0.5: context key was renamed from `quonfig.logger-name`
|
|
12
|
+
# (dotted snake_case, flat) to `quonfig-sdk-logging.key` (nested, verbatim
|
|
13
|
+
# class name). Normalization was removed — logger names are passed through
|
|
14
|
+
# as-is.
|
|
9
15
|
class TestSemanticLoggerFilter < Minitest::Test
|
|
10
16
|
CONFIG_KEY = 'log-levels.my-app'
|
|
11
17
|
|
|
12
18
|
# FakeClient lets us assert the exact key + context the filter passes to
|
|
13
|
-
# the SDK without standing up a full datadir.
|
|
14
|
-
# the *specific mechanism* this bead is verifying — if the filter ever
|
|
15
|
-
# regresses to a per-logger key, this captured request goes wrong.
|
|
19
|
+
# the SDK without standing up a full datadir.
|
|
16
20
|
class FakeClient
|
|
17
21
|
attr_reader :calls
|
|
18
22
|
|
|
@@ -43,7 +47,7 @@ class TestSemanticLoggerFilter < Minitest::Test
|
|
|
43
47
|
assert_equal 1, client.calls.size
|
|
44
48
|
assert_equal CONFIG_KEY, client.calls.first[:key]
|
|
45
49
|
ctx = client.calls.first[:context]
|
|
46
|
-
assert_equal({ 'quonfig' => { '
|
|
50
|
+
assert_equal({ 'quonfig-sdk-logging' => { 'key' => 'MyApp::Foo::Bar' } }, ctx)
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def test_passes_through_when_level_meets_configured_minimum
|
|
@@ -71,19 +75,22 @@ class TestSemanticLoggerFilter < Minitest::Test
|
|
|
71
75
|
assert_equal true, filter.call(make_log('Anything', :debug))
|
|
72
76
|
end
|
|
73
77
|
|
|
74
|
-
def
|
|
78
|
+
def test_logger_name_passed_through_verbatim
|
|
79
|
+
# Normalization is gone. Native Ruby class names are preserved as-is,
|
|
80
|
+
# which matches how sdk-node and sdk-go pass the logger path.
|
|
75
81
|
filter, client = filter_for(:debug)
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
'MyApp::Foo::Bar'
|
|
79
|
-
'HTMLParser'
|
|
80
|
-
'foo'
|
|
81
|
-
'A::B::CDPath'
|
|
82
|
-
|
|
83
|
+
[
|
|
84
|
+
'MyApp::Foo::Bar',
|
|
85
|
+
'HTMLParser',
|
|
86
|
+
'foo',
|
|
87
|
+
'A::B::CDPath',
|
|
88
|
+
'MyApp::Services::Auth'
|
|
89
|
+
].each do |raw|
|
|
83
90
|
client.calls.clear
|
|
84
91
|
filter.call(make_log(raw, :info))
|
|
85
|
-
assert_equal
|
|
86
|
-
"
|
|
92
|
+
assert_equal raw, client.calls.first[:context]['quonfig-sdk-logging']['key'],
|
|
93
|
+
"logger name should be passed through verbatim: #{raw.inspect}"
|
|
87
94
|
end
|
|
88
95
|
end
|
|
89
96
|
|
|
@@ -100,6 +107,20 @@ class TestSemanticLoggerFilter < Minitest::Test
|
|
|
100
107
|
'Filter should call exactly the configured key, never derived per-logger keys'
|
|
101
108
|
end
|
|
102
109
|
|
|
110
|
+
def test_normalize_method_is_gone
|
|
111
|
+
# The old normalize() method converted "MyApp::Foo" → "my_app.foo".
|
|
112
|
+
# It is intentionally removed so callers see native Ruby class names in
|
|
113
|
+
# context telemetry and rule matching.
|
|
114
|
+
refute Quonfig::SemanticLoggerFilter.instance_methods.include?(:normalize),
|
|
115
|
+
'normalize() should be removed — logger names are passed through as-is'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_context_key_constant_is_new_shape
|
|
119
|
+
# Sanity check that the context key constant exposes the new shape.
|
|
120
|
+
assert_equal 'quonfig-sdk-logging', Quonfig::SemanticLoggerFilter::LOGGER_CONTEXT_NAME
|
|
121
|
+
assert_equal 'key', Quonfig::SemanticLoggerFilter::LOGGER_CONTEXT_KEY_PROP
|
|
122
|
+
end
|
|
123
|
+
|
|
103
124
|
def test_all_six_levels_mapped_correctly
|
|
104
125
|
expected = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5 }
|
|
105
126
|
assert_equal expected, Quonfig::SemanticLoggerFilter::LEVELS
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quonfig
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeff Dwyer
|
|
@@ -263,6 +263,7 @@ files:
|
|
|
263
263
|
- test/test_bound_client.rb
|
|
264
264
|
- test/test_caching_http_connection.rb
|
|
265
265
|
- test/test_client.rb
|
|
266
|
+
- test/test_client_network_mode.rb
|
|
266
267
|
- test/test_config_loader.rb
|
|
267
268
|
- test/test_context.rb
|
|
268
269
|
- test/test_datadir.rb
|