Class: Quonfig::Client

Inherits:
Object
  • Object
show all
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:

  1. datadir: (offline) – load a workspace from the local filesystem.

  2. store: (test harness) – caller-supplied ConfigStore, no I/O.

  3. network mode (default) – HTTP fetch from api_urls populates 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

Instance Method Summary collapse

Constructor Details

#initialize(options = nil, store: nil, **option_kwargs) ⇒ Client

Returns a new instance of Client.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/quonfig/client.rb', line 26

def initialize(options = nil, store: nil, **option_kwargs)
  @options =
    if options.is_a?(Quonfig::Options)
      options
    elsif options.is_a?(Hash)
      Quonfig::Options.new(options.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_thread = nil
  @stopped = false
  @telemetry_reporter = nil

  # 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
  else
    initialize_network_mode
  end

  initialize_telemetry
end

Instance Attribute Details

#config_loaderObject (readonly)

Returns the value of attribute config_loader.



23
24
25
# File 'lib/quonfig/client.rb', line 23

def config_loader
  @config_loader
end

#evaluatorObject (readonly)

Returns the value of attribute evaluator.



23
24
25
# File 'lib/quonfig/client.rb', line 23

def evaluator
  @evaluator
end

#instance_hashObject (readonly)

Returns the value of attribute instance_hash.



23
24
25
# File 'lib/quonfig/client.rb', line 23

def instance_hash
  @instance_hash
end

#optionsObject (readonly)

Returns the value of attribute options.



23
24
25
# File 'lib/quonfig/client.rb', line 23

def options
  @options
end

#resolverObject (readonly)

Returns the value of attribute resolver.



23
24
25
# File 'lib/quonfig/client.rb', line 23

def resolver
  @resolver
end

#storeObject (readonly)

Returns the value of attribute store.



23
24
25
# File 'lib/quonfig/client.rb', line 23

def store
  @store
end

#telemetry_reporterObject (readonly)

Returns the value of attribute telemetry_reporter.



23
24
25
# File 'lib/quonfig/client.rb', line 23

def telemetry_reporter
  @telemetry_reporter
end

Instance Method Details

#defined?(key) ⇒ Boolean

Returns:

  • (Boolean)


144
145
146
# File 'lib/quonfig/client.rb', line 144

def defined?(key)
  !@store.get(key).nil?
end

#enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED) ⇒ Boolean

Returns:

  • (Boolean)


139
140
141
142
# File 'lib/quonfig/client.rb', line 139

def enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED)
  value = get(feature_name, false, jit_context)
  [true, 'true'].include?(value)
end

#forkObject



281
282
283
# File 'lib/quonfig/client.rb', line 281

def fork
  self.class.new(@options.for_fork)
end

#get(key, default = NO_DEFAULT_PROVIDED, jit_context = NO_DEFAULT_PROVIDED) ⇒ Object

—- Lookup ——————————————————–



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/quonfig/client.rb', line 61

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



91
92
93
# File 'lib/quonfig/client.rb', line 91

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.



115
116
117
# File 'lib/quonfig/client.rb', line 115

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



99
100
101
# File 'lib/quonfig/client.rb', line 99

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



87
88
89
# File 'lib/quonfig/client.rb', line 87

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



127
128
129
# File 'lib/quonfig/client.rb', line 127

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



83
84
85
# File 'lib/quonfig/client.rb', line 83

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



123
124
125
# File 'lib/quonfig/client.rb', line 123

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



103
104
105
# File 'lib/quonfig/client.rb', line 103

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



135
136
137
# File 'lib/quonfig/client.rb', line 135

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



79
80
81
# File 'lib/quonfig/client.rb', line 79

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



119
120
121
# File 'lib/quonfig/client.rb', line 119

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



95
96
97
# File 'lib/quonfig/client.rb', line 95

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



131
132
133
# File 'lib/quonfig/client.rb', line 131

def get_string_list_details(key, context: NO_DEFAULT_PROVIDED)
  evaluate_details(key, :string_list, context)
end

#in_context(properties) ⇒ Object

—- Context binding ———————————————-



154
155
156
157
# File 'lib/quonfig/client.rb', line 154

def in_context(properties)
  bound = Quonfig::BoundClient.new(self, properties)
  block_given? ? yield(bound) : bound
end

#inspectObject



285
286
287
# File 'lib/quonfig/client.rb', line 285

def inspect
  "#<Quonfig::Client:#{object_id} environment=#{@options.environment.inspect}>"
end

#keysObject



148
149
150
# File 'lib/quonfig/client.rb', line 148

def keys
  @store.keys
end

#logger_keyObject

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.



200
201
202
# File 'lib/quonfig/client.rb', line 200

def logger_key
  @options.logger_key
end

#on_update(&block) ⇒ Object



256
257
258
# File 'lib/quonfig/client.rb', line 256

def on_update(&block)
  @on_update = block
end

#semantic_logger_filter(config_key:) ⇒ Object

—- Filters & helpers ——————————————–



169
170
171
172
# File 'lib/quonfig/client.rb', line 169

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.

Parameters:

  • logger_path (String)

    native logger name (typically a class name).

  • desired_level (Symbol, String)

    the level the caller wants to emit at (:trace, :debug, :info, :warn, :error, :fatal).

  • contexts (Hash) (defaults to: {})

    optional extra context to merge with the injected logger context.

Returns:

  • (Boolean)

    true if the message should be emitted.



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/quonfig/client.rb', line 231

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.

Parameters:

  • logger_name (String, nil) (defaults to: nil)

    fallback logger identifier used when progname isn’t supplied by the Logger call site. If both are present, logger_name wins.

Returns:

  • (Proc)

    a (severity, datetime, progname, msg) -> String proc.



193
194
195
# File 'lib/quonfig/client.rb', line 193

def stdlib_formatter(logger_name: nil)
  Quonfig::StdlibFormatter.build(self, logger_name: logger_name)
end

#stopObject



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/quonfig/client.rb', line 260

def stop
  @stopped = true
  begin
    @sse_client&.close
  rescue StandardError => e
    LOG.debug "Error closing SSE client: #{e.message}"
  end
  @sse_client = nil

  thread = @poll_thread
  @poll_thread = nil
  thread&.kill

  begin
    @telemetry_reporter&.stop
  rescue StandardError => e
    LOG.debug "Error stopping telemetry reporter: #{e.message}"
  end
  @telemetry_reporter = nil
end

#with_context(properties, &block) ⇒ Object



159
160
161
162
163
164
165
# File 'lib/quonfig/client.rb', line 159

def with_context(properties, &block)
  if block_given?
    in_context(properties, &block)
  else
    Quonfig::BoundClient.new(self, properties)
  end
end