Class: Quonfig::Client
- Inherits:
-
Object
- Object
- Quonfig::Client
- Defined in:
- lib/quonfig/client.rb
Overview
Public Quonfig SDK client.
Wires the JSON stack: Quonfig::ConfigStore + Quonfig::Evaluator + Quonfig::Resolver. Three modes are supported:
-
datadir:(offline) – load a workspace from the local filesystem. -
store:(test harness) – caller-supplied ConfigStore, no I/O. -
network mode (default) – HTTP fetch from
api_urlspopulates the ConfigStore, then (if enabled) an SSE subscription keeps it live.
Network mode is the happy path for production SDK usage. The protobuf stack was retired in qfg-dk6.32; HTTP + SSE were wired back through Client in qfg-s7h.
Constant Summary collapse
- LOG =
Quonfig::InternalLogger.new(self)
Instance Attribute Summary collapse
-
#config_loader ⇒ Object
readonly
Returns the value of attribute config_loader.
-
#evaluator ⇒ Object
readonly
Returns the value of attribute evaluator.
-
#instance_hash ⇒ Object
readonly
Returns the value of attribute instance_hash.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
-
#resolver ⇒ Object
readonly
Returns the value of attribute resolver.
-
#store ⇒ Object
readonly
Returns the value of attribute store.
-
#telemetry_reporter ⇒ Object
readonly
Returns the value of attribute telemetry_reporter.
Class Method Summary collapse
-
.each_instance(&block) ⇒ Object
Iterate live Client instances.
- .register_instance(client) ⇒ Object
Instance Method Summary collapse
-
#after_fork_in_child ⇒ Object
qfg-ryov: post-fork (in child) hook.
-
#before_fork_in_parent ⇒ Object
qfg-ryov: pre-fork hook.
-
#connection_state ⇒ Object
Aggregate connection state.
- #defined?(key) ⇒ Boolean
- #enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED) ⇒ Boolean
- #fork ⇒ Object
-
#get(key, default = NO_DEFAULT_PROVIDED, jit_context = NO_DEFAULT_PROVIDED) ⇒ Object
—- Lookup ——————————————————–.
- #get_bool(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
-
#get_bool_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
—- Details getters ———————————————-.
- #get_duration(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_float(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_float_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_int(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_int_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_json(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_json_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_string(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_string_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_string_list(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
- #get_string_list_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
-
#in_context(properties) ⇒ Object
—- Context binding ———————————————-.
-
#initialize(options = nil, store: nil, **option_kwargs) ⇒ Client
constructor
A new instance of Client.
- #inspect ⇒ Object
- #keys ⇒ Object
-
#last_successful_refresh ⇒ Object
Wall-clock time of the last installed envelope (any source: datadir, initial HTTP fetch, SSE, or polling fallback).
-
#logger_key ⇒ Object
The configured
logger_keyfrom Options — the Quonfig config key the higher-levelshould_log?helper evaluates per-logger. - #on_update(&block) ⇒ Object
-
#semantic_logger_filter(config_key:) ⇒ Object
—- Filters & helpers ——————————————–.
-
#should_log?(logger_path:, desired_level:, contexts: {}) ⇒ Boolean
Higher-level log-level check — a convenience on top of the primitive
get. -
#stdlib_formatter(logger_name: nil) ⇒ Proc
Build a formatter Proc for Ruby’s built-in
::Logger. - #stop ⇒ Object
-
#terminal_failure? ⇒ Boolean
qfg-i5xv: true once the SSE layer has classified an HTTP response as terminal (401/403/404) — bad SDK key, revoked workspace permission, or wrong endpoint.
- #with_context(properties, &block) ⇒ Object
-
#worker_restart_total(layer: nil) ⇒ Object
quonfig_sdk_worker_restart_total counter (Tier 1 supervisor contract).
Constructor Details
#initialize(options = nil, store: nil, **option_kwargs) ⇒ Client
Returns a new instance of Client.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/quonfig/client.rb', line 49 def initialize( = nil, store: nil, **option_kwargs) @options = if .is_a?(Quonfig::Options) elsif .is_a?(Hash) Quonfig::Options.new(.merge(option_kwargs)) else Quonfig::Options.new(option_kwargs) end Quonfig::InternalLogger.user_logger = @options.logger if @options.logger @global_context = build_initial_global_context(@options) @instance_hash = SecureRandom.uuid @store = store || Quonfig::ConfigStore.new @evaluator = Quonfig::Evaluator.new(@store, env_id: @options.environment) @resolver = Quonfig::Resolver.new(@store, @evaluator) @semantic_logger_filters = {} @sse_client = nil @poll_supervisor = nil @stopped = false @telemetry_reporter = nil @state_mutex = Mutex.new @last_successful_refresh = nil @sse_state = :idle @sse_ever_connected = false @fallback_engage_timer = nil @sse_terminal_failure = false # If the caller injected a store, we're in test/bootstrap mode; skip I/O. return if store if @options.datadir load_datadir_into_store start_datadir_watcher if @options.data_dir_auto_reload else initialize_network_mode end initialize_telemetry # Register only for non-store-injected clients (a caller-supplied store # is the test/bootstrap path; the fork hook does not apply there). self.class.register_instance(self) unless store end |
Instance Attribute Details
#config_loader ⇒ Object (readonly)
Returns the value of attribute config_loader.
46 47 48 |
# File 'lib/quonfig/client.rb', line 46 def config_loader @config_loader end |
#evaluator ⇒ Object (readonly)
Returns the value of attribute evaluator.
46 47 48 |
# File 'lib/quonfig/client.rb', line 46 def evaluator @evaluator end |
#instance_hash ⇒ Object (readonly)
Returns the value of attribute instance_hash.
46 47 48 |
# File 'lib/quonfig/client.rb', line 46 def instance_hash @instance_hash end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
46 47 48 |
# File 'lib/quonfig/client.rb', line 46 def @options end |
#resolver ⇒ Object (readonly)
Returns the value of attribute resolver.
46 47 48 |
# File 'lib/quonfig/client.rb', line 46 def resolver @resolver end |
#store ⇒ Object (readonly)
Returns the value of attribute store.
46 47 48 |
# File 'lib/quonfig/client.rb', line 46 def store @store end |
#telemetry_reporter ⇒ Object (readonly)
Returns the value of attribute telemetry_reporter.
46 47 48 |
# File 'lib/quonfig/client.rb', line 46 def telemetry_reporter @telemetry_reporter end |
Class Method Details
.each_instance(&block) ⇒ Object
Iterate live Client instances. Used by Quonfig::ForkSafety.
37 38 39 |
# File 'lib/quonfig/client.rb', line 37 def each_instance(&block) @instances_mutex.synchronize { @instances.keys }.each(&block) end |
.register_instance(client) ⇒ Object
41 42 43 |
# File 'lib/quonfig/client.rb', line 41 def register_instance(client) @instances_mutex.synchronize { @instances[client] = true } end |
Instance Method Details
#after_fork_in_child ⇒ Object
qfg-ryov: post-fork (in child) hook. Re-establish whatever threaded components the client had pre-fork. No-op if the client was already stopped (the customer asked for it to be dead — do not resurrect), or if the client is in datadir mode (no threaded components to start).
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 |
# File 'lib/quonfig/client.rb', line 318 def after_fork_in_child return if @stopped if @options.datadir start_datadir_watcher if @options.data_dir_auto_reload return end return if @config_loader.nil? # never finished network init (e.g. invalid key) # SSE state machine carries flags that no longer apply in the child # (the parent had connected, the parent had errored, etc.). Reset. @state_mutex.synchronize do @sse_state = :idle @sse_ever_connected = false @sse_terminal_failure = false end sse_started = @options.enable_sse && start_sse start_polling if @options.enable_polling && !sse_started restart_telemetry_in_child end |
#before_fork_in_parent ⇒ Object
qfg-ryov: pre-fork hook. Close the SSE worker, polling supervisor, telemetry reporter, and any fallback-engage timer. Idempotent — calling twice is safe. Does NOT set @stopped: the client is still expected to be usable post-fork via after_fork_in_child.
Why this matters: Ruby threads do not survive fork(2). If we let the child inherit a live Net::HTTP socket, both processes read from the same fd and corrupt each other’s bytes. Closing in the parent before fork is the only safe shape.
308 309 310 311 312 |
# File 'lib/quonfig/client.rb', line 308 def before_fork_in_parent return if @stopped tear_down_threaded_components! end |
#connection_state ⇒ Object
Aggregate connection state. Returns one of:
-
:initializing— no envelope has been installed and SSE is not yet connected. -
:connected— SSE is live, or the SDK is delivering configs from a loaded envelope (datadir mode or post-initial-fetch with no SSE). -
:disconnected—stopwas called, or SSE errored and no fallback poller is active. -
:falling_back— the Layer 2 HTTP polling supervisor is alive and serving as the active update channel.
**Diagnostic only.** Do NOT wire this into a Kubernetes liveness probe — see the README “Diagnostic health signals” section.
Contract: integration-test-data/chaos/supervisor-test-contract.md (Test 6).
389 390 391 392 393 394 395 396 397 398 399 400 |
# File 'lib/quonfig/client.rb', line 389 def connection_state @state_mutex.synchronize do next :disconnected if @stopped next :falling_back if @poll_supervisor&.alive? next :connected if @sse_state == :connected next :disconnected if @sse_state == :error # No SSE state change yet: state is driven by whether any envelope # has been installed (datadir / initial fetch). @last_successful_refresh.nil? ? :initializing : :connected end end |
#defined?(key) ⇒ Boolean
178 179 180 |
# File 'lib/quonfig/client.rb', line 178 def defined?(key) !@store.get(key).nil? end |
#enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED) ⇒ Boolean
173 174 175 176 |
# File 'lib/quonfig/client.rb', line 173 def enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED) value = get(feature_name, false, jit_context) [true, 'true'].include?(value) end |
#fork ⇒ Object
402 403 404 |
# File 'lib/quonfig/client.rb', line 402 def fork self.class.new(@options.for_fork) end |
#get(key, default = NO_DEFAULT_PROVIDED, jit_context = NO_DEFAULT_PROVIDED) ⇒ Object
—- Lookup ——————————————————–
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/quonfig/client.rb', line 95 def get(key, default = NO_DEFAULT_PROVIDED, jit_context = NO_DEFAULT_PROVIDED) ctx = build_context(jit_context) record_context_for_telemetry(ctx) result = begin @resolver.get(key, ctx) rescue Quonfig::Errors::MissingDefaultError # The Resolver raises (matching Quonfig.get_or_raise semantics). # The Client's get applies the caller-provided default *or* the # configured on_no_default policy via handle_missing. nil end return handle_missing(key, default) if result.nil? record_evaluation_for_telemetry(result) result.unwrapped_value end |
#get_bool(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
125 126 127 |
# File 'lib/quonfig/client.rb', line 125 def get_bool(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) typed_get(key, :bool, default: default, context: context) end |
#get_bool_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
—- Details getters ———————————————-
Mirrors the typed getters above but returns a Quonfig::EvaluationDetails carrying the OpenFeature-aligned resolution reason (“STATIC”, “TARGETING_MATCH”, “SPLIT”, “DEFAULT”, or “ERROR”) plus an error_code/error_message on the error path. These methods never raise — exceptions are caught and rendered as ERROR details.
149 150 151 |
# File 'lib/quonfig/client.rb', line 149 def get_bool_details(key, context: NO_DEFAULT_PROVIDED) evaluate_details(key, :bool, context) end |
#get_duration(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
133 134 135 |
# File 'lib/quonfig/client.rb', line 133 def get_duration(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) typed_get(key, :duration, default: default, context: context) end |
#get_float(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
121 122 123 |
# File 'lib/quonfig/client.rb', line 121 def get_float(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) typed_get(key, Float, default: default, context: context) end |
#get_float_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
161 162 163 |
# File 'lib/quonfig/client.rb', line 161 def get_float_details(key, context: NO_DEFAULT_PROVIDED) evaluate_details(key, Float, context) end |
#get_int(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
117 118 119 |
# File 'lib/quonfig/client.rb', line 117 def get_int(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) typed_get(key, Integer, default: default, context: context) end |
#get_int_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
157 158 159 |
# File 'lib/quonfig/client.rb', line 157 def get_int_details(key, context: NO_DEFAULT_PROVIDED) evaluate_details(key, Integer, context) end |
#get_json(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
137 138 139 |
# File 'lib/quonfig/client.rb', line 137 def get_json(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) typed_get(key, :json, default: default, context: context) end |
#get_json_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
169 170 171 |
# File 'lib/quonfig/client.rb', line 169 def get_json_details(key, context: NO_DEFAULT_PROVIDED) evaluate_details(key, :json, context) end |
#get_string(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
113 114 115 |
# File 'lib/quonfig/client.rb', line 113 def get_string(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) typed_get(key, String, default: default, context: context) end |
#get_string_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
153 154 155 |
# File 'lib/quonfig/client.rb', line 153 def get_string_details(key, context: NO_DEFAULT_PROVIDED) evaluate_details(key, String, context) end |
#get_string_list(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) ⇒ Object
129 130 131 |
# File 'lib/quonfig/client.rb', line 129 def get_string_list(key, default: NO_DEFAULT_PROVIDED, context: NO_DEFAULT_PROVIDED) typed_get(key, :string_list, default: default, context: context) end |
#get_string_list_details(key, context: NO_DEFAULT_PROVIDED) ⇒ Object
165 166 167 |
# File 'lib/quonfig/client.rb', line 165 def get_string_list_details(key, context: NO_DEFAULT_PROVIDED) evaluate_details(key, :string_list, context) end |
#in_context(properties) ⇒ Object
—- Context binding ———————————————-
188 189 190 191 |
# File 'lib/quonfig/client.rb', line 188 def in_context(properties) bound = Quonfig::BoundClient.new(self, properties) block_given? ? yield(bound) : bound end |
#inspect ⇒ Object
406 407 408 |
# File 'lib/quonfig/client.rb', line 406 def inspect "#<Quonfig::Client:#{object_id} environment=#{@options.environment.inspect}>" end |
#keys ⇒ Object
182 183 184 |
# File 'lib/quonfig/client.rb', line 182 def keys @store.keys end |
#last_successful_refresh ⇒ Object
Wall-clock time of the last installed envelope (any source: datadir, initial HTTP fetch, SSE, or polling fallback). nil before the first install. Preserved after stop.
**Diagnostic only.** Do NOT wire this into a Kubernetes liveness probe — a transient network blip will trip any freshness threshold and cause a rolling restart cascade. See the README “Diagnostic health signals” section.
Contract: integration-test-data/chaos/supervisor-test-contract.md (Test 6).
370 371 372 |
# File 'lib/quonfig/client.rb', line 370 def last_successful_refresh @state_mutex.synchronize { @last_successful_refresh } end |
#logger_key ⇒ Object
The configured logger_key from Options — the Quonfig config key the higher-level should_log? helper evaluates per-logger. nil if the client was not configured for dynamic log levels.
234 235 236 |
# File 'lib/quonfig/client.rb', line 234 def logger_key @options.logger_key end |
#on_update(&block) ⇒ Object
290 291 292 |
# File 'lib/quonfig/client.rb', line 290 def on_update(&block) @on_update = block end |
#semantic_logger_filter(config_key:) ⇒ Object
—- Filters & helpers ——————————————–
203 204 205 206 |
# File 'lib/quonfig/client.rb', line 203 def semantic_logger_filter(config_key:) @semantic_logger_filters[config_key] ||= Quonfig::SemanticLoggerFilter.new(self, config_key: config_key) end |
#should_log?(logger_path:, desired_level:, contexts: {}) ⇒ Boolean
Higher-level log-level check — a convenience on top of the primitive get. Evaluates the client’s logger_key config and returns whether a message at desired_level should be emitted for logger_path.
The SDK injects logger_path under the quonfig-sdk-logging named context with property key so a single log-level config can drive per-logger overrides via the normal rule engine (e.g. PROP_STARTS_WITH_ONE_OF “MyApp::Services::”).
logger_path is passed through verbatim — the SDK does not normalize it. Callers may pass any identifier shape their host language prefers (dotted, colon, slash, etc.) and author matching rules in the config against that exact shape.
Parallels sdk-node’s shouldLog({loggerPath}) and sdk-go’s ShouldLogPath.
Raises Quonfig::Error if logger_key was not set on the client —use semantic_logger_filter(config_key:) directly if you want to evaluate a specific key without declaring it at init time.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/quonfig/client.rb', line 265 def should_log?(logger_path:, desired_level:, contexts: {}) unless logger_key raise Quonfig::Error, 'logger_key must be set at init to use should_log?(logger_path:, ...). ' \ 'Pass `logger_key:` to Quonfig::Options.new, or call ' \ 'semantic_logger_filter(config_key:) / get(config_key) directly.' end logger_context = { Quonfig::SemanticLoggerFilter::LOGGER_CONTEXT_NAME => { Quonfig::SemanticLoggerFilter::LOGGER_CONTEXT_KEY_PROP => logger_path } } merged = merge_contexts(normalize_context(contexts), logger_context) configured = get(logger_key, nil, merged) return true if configured.nil? desired_severity = Quonfig::SemanticLoggerFilter::LEVELS[normalize_log_level(desired_level)] || Quonfig::SemanticLoggerFilter::LEVELS[:debug] min_severity = Quonfig::SemanticLoggerFilter::LEVELS[normalize_log_level(configured)] || Quonfig::SemanticLoggerFilter::LEVELS[:debug] desired_severity >= min_severity end |
#stdlib_formatter(logger_name: nil) ⇒ Proc
Build a formatter Proc for Ruby’s built-in ::Logger. The returned proc honors dynamic log levels from the client’s logger_key config: for each log call, it evaluates should_log? and either formats the record or returns an empty string (suppressing output).
Matches ReforgeHQ’s stdlib_formatter API name (snake_case).
Usage:
logger = ::Logger.new($stdout)
logger.formatter = client.stdlib_formatter # uses progname
logger.formatter = client.stdlib_formatter(logger_name: 'MyApp') # fixed name
Raises Quonfig::Error if logger_key was not set at init — parallels should_log?‘s behavior.
227 228 229 |
# File 'lib/quonfig/client.rb', line 227 def stdlib_formatter(logger_name: nil) Quonfig::StdlibFormatter.build(self, logger_name: logger_name) end |
#stop ⇒ Object
294 295 296 297 |
# File 'lib/quonfig/client.rb', line 294 def stop @stopped = true tear_down_threaded_components! end |
#terminal_failure? ⇒ Boolean
qfg-i5xv: true once the SSE layer has classified an HTTP response as terminal (401/403/404) — bad SDK key, revoked workspace permission, or wrong endpoint. The classification latches: the SDK will not auto-recover, and a customer-supplied retry must rebuild the client. Surfaced for operator alerting; ‘connection_state` still reports `:disconnected` to honor the documented connection_state vocabulary (supervisor-test-contract.md §“connectionState()” — values fixed).
547 548 549 |
# File 'lib/quonfig/client.rb', line 547 def terminal_failure? @state_mutex.synchronize { @sse_terminal_failure } end |
#with_context(properties, &block) ⇒ Object
193 194 195 196 197 198 199 |
# File 'lib/quonfig/client.rb', line 193 def with_context(properties, &block) if block_given? in_context(properties, &block) else Quonfig::BoundClient.new(self, properties) end end |
#worker_restart_total(layer: nil) ⇒ Object
quonfig_sdk_worker_restart_total counter (Tier 1 supervisor contract). Layer 1 (SSE) is tracked on Quonfig::SSEConfigClient#restart_total —incremented once per reconnect attempt by the SDK-owned reconnect loop (qfg-35sm). Layer 2 (HTTP polling fallback) is wired through Quonfig::WorkerSupervisor.
Pass layer: (‘1’ or ‘2’) to read a single layer; default returns the sum across both layers so the chaos harness (and operators) can pull per-layer values explicitly while preserving the previous single-number diagnostic surface.
352 353 354 355 356 357 358 |
# File 'lib/quonfig/client.rb', line 352 def worker_restart_total(layer: nil) case layer&.to_s when '1' then sse_restart_total when '2' then poll_restart_total else sse_restart_total + poll_restart_total end end |