{"id":49754078,"url":"https://github.com/quonfig/sdk-ruby","last_synced_at":"2026-05-10T17:01:15.550Z","repository":{"id":353159965,"uuid":"1215284514","full_name":"quonfig/sdk-ruby","owner":"quonfig","description":"Quonfig SDK for Ruby — feature flags, live config, and dynamic log levels","archived":false,"fork":false,"pushed_at":"2026-05-03T11:31:12.000Z","size":1138,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-03T13:21:37.889Z","etag":null,"topics":["configuration","feature-flag","feature-flags","quonfig","ruby","sdk"],"latest_commit_sha":null,"homepage":"https://quonfig.com","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/quonfig.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-19T18:08:08.000Z","updated_at":"2026-05-03T11:30:17.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/quonfig/sdk-ruby","commit_stats":null,"previous_names":["quonfig/sdk-ruby"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/quonfig/sdk-ruby","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quonfig%2Fsdk-ruby","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quonfig%2Fsdk-ruby/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quonfig%2Fsdk-ruby/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quonfig%2Fsdk-ruby/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/quonfig","download_url":"https://codeload.github.com/quonfig/sdk-ruby/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quonfig%2Fsdk-ruby/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32860465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-10T13:40:02.631Z","status":"ssl_error","status_checked_at":"2026-05-10T13:40:02.145Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["configuration","feature-flag","feature-flags","quonfig","ruby","sdk"],"created_at":"2026-05-10T17:01:14.568Z","updated_at":"2026-05-10T17:01:15.539Z","avatar_url":"https://github.com/quonfig.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# quonfig\n\nRuby SDK for [Quonfig](https://quonfig.com) — Feature Flags, Live Config, and Dynamic Log Levels.\n\n\u003e **Note:** This SDK is pre-1.0 and the API is not yet stable.\n\n## Installation\n\nAdd the gem to your Gemfile:\n\n```ruby\ngem 'quonfig'\n```\n\nOr install directly:\n\n```bash\ngem install quonfig\n```\n\n## Quickstart\n\n```ruby\nrequire 'quonfig'\n\nclient = Quonfig::Client.new(sdk_key: ENV['QUONFIG_BACKEND_SDK_KEY'])\n\n# Feature flags\nif client.enabled?('new-dashboard')\n  # show new dashboard\nend\n\n# Typed config values\nlimit   = client.get_int('rate-limit')\nname    = client.get_string('app.display-name')\nregions = client.get_string_list('allowed-regions')\n\n# Context-aware evaluation — pass a context hash as the last argument\nvalue = client.get_string('homepage-hero', user: { key: 'user-123', country: 'US' })\n```\n\n## Context\n\nContexts are hashes grouped by scope (`user`, `team`, `device`, etc.). You can\nattach a context in three ways:\n\n### 1. Per-call context\n\n```ruby\nclient.get_bool('beta-feature', user: { key: 'user-123', plan: 'pro' })\n```\n\n### 2. `in_context` block\n\nEverything evaluated inside the block sees the supplied context. The block's\nreturn value is returned from `in_context`.\n\n```ruby\nresult = client.in_context(user: { key: 'user-123', plan: 'pro' }) do |bound|\n  {\n    hero:   bound.get_string('homepage-hero'),\n    limit:  bound.get_int('rate-limit'),\n    beta?:  bound.enabled?('beta-feature')\n  }\nend\n```\n\n### 3. `with_context` — BoundClient for repeated lookups\n\n`with_context` returns an immutable `BoundClient` that carries the context on\nevery call. Useful when you want to pass a context-bound handle down the stack.\n\n```ruby\nbound = client.with_context(user: { key: 'user-123', plan: 'pro' })\n\nbound.get_string('homepage-hero')\nbound.enabled?('beta-feature')\nbound.get_int('rate-limit')\n```\n\n## Datadir / offline mode\n\nFor tests, CI, or air-gapped environments, point the client at a local workspace\ndirectory instead of the Quonfig API. In datadir mode the SDK loads JSON config\nfiles from disk and performs no network I/O.\n\n```ruby\nclient = Quonfig::Client.new(\n  datadir:     '/path/to/workspace',\n  environment: 'production'\n)\n\nclient.get_bool('feature-x')\n```\n\nYou can also set `QUONFIG_DIR` in the environment and omit the `datadir:`\noption; when `QUONFIG_DIR` is set the SDK switches to datadir mode\nautomatically. `environment` is required in datadir mode — it can be provided\nvia the option or via `QUONFIG_ENVIRONMENT`.\n\n```bash\nexport QUONFIG_DIR=/path/to/workspace\nexport QUONFIG_ENVIRONMENT=production\n```\n\n```ruby\nclient = Quonfig::Client.new  # reads QUONFIG_DIR + QUONFIG_ENVIRONMENT\n```\n\n## Environment variables\n\n| Variable                    | Purpose                                                                                  |\n|-----------------------------|------------------------------------------------------------------------------------------|\n| `QUONFIG_BACKEND_SDK_KEY`   | SDK key used to authenticate against the Quonfig API. Used when `sdk_key:` is omitted.   |\n| `QUONFIG_DIR`               | Path to a workspace directory. When set, the SDK runs in datadir/offline mode.           |\n| `QUONFIG_ENVIRONMENT`       | Environment name (`production`, `staging`, `development`) evaluated in datadir mode.     |\n| `QUONFIG_DOMAIN`            | Base domain used to derive api, sse, and telemetry URLs. Defaults to `quonfig.com`. Set to `quonfig-staging.com` to point at staging. Explicit `api_urls:` / `telemetry_url:` kwargs override this. |\n\n## Constructor options\n\n```ruby\nQuonfig::Client.new(\n  sdk_key:         '...',                          # required unless QUONFIG_BACKEND_SDK_KEY is set\n  api_urls:        ['https://primary.quonfig.com', 'https://secondary.quonfig.com'],\n  telemetry_url:   'https://telemetry.quonfig.com',\n  enable_sse:      true,\n  enable_polling:  false,\n  poll_interval:   60,\n  init_timeout:    10,\n  on_no_default:   :error,\n  global_context:  {},\n  datadir:         '/path/to/workspace',\n  environment:     'production'\n)\n```\n\n| Option            | Type                       | Default                                                             | Description                                                                                       |\n|-------------------|----------------------------|---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|\n| `sdk_key`         | `String`                   | `ENV['QUONFIG_BACKEND_SDK_KEY']`                                    | SDK key for API authentication.                                                                   |\n| `api_urls`        | `Array\u003cString\u003e`            | `[\"https://primary.${QUONFIG_DOMAIN}\", \"https://secondary.${QUONFIG_DOMAIN}\"]` | Ordered list of API base URLs to try. SSE stream URLs are derived by prepending `stream.` to each hostname. Defaults derive from `QUONFIG_DOMAIN` (default `quonfig.com`). |\n| `telemetry_url`   | `String`                   | `https://telemetry.${QUONFIG_DOMAIN}`                                          | Base URL for the telemetry service. Default derives from `QUONFIG_DOMAIN`.                        |\n| `enable_sse`      | `Boolean`                  | `true`                                                              | Receive real-time updates over Server-Sent Events.                                                |\n| `enable_polling`  | `Boolean`                  | `false`                                                             | Poll the API on an interval as a fallback.                                                        |\n| `poll_interval`   | `Integer` (seconds)        | `60`                                                                | Polling interval when `enable_polling` is `true`.                                                 |\n| `init_timeout`    | `Integer` (seconds)        | `10`                                                                | Maximum time to wait for the initial config load.                                                 |\n| `on_no_default`   | `Symbol`                   | `:error`                                                            | Behavior when a key has no value and no default: `:error`, `:warn`, or `:ignore`.                 |\n| `global_context`  | `Hash`                     | `{}`                                                                | Context applied to every evaluation.                                                              |\n| `datadir`         | `String`                   | `ENV['QUONFIG_DIR']`                                                | Path to a local workspace. When set, the SDK runs offline from disk.                              |\n| `environment`     | `String`                   | `ENV['QUONFIG_ENVIRONMENT']`                                        | Environment to evaluate in datadir mode. Required when `datadir` is set.                          |\n| `logger`          | Logger-like object         | `nil`                                                               | Optional host-app logger (e.g. `Rails.logger`). Must respond to `debug`/`info`/`warn`/`error`. When set, all SDK warnings/errors flow through this logger instead of the default stderr / SemanticLogger backend. |\n\n## Typed getters\n\nEach typed getter takes a config key and an optional context hash. If the key\nis missing or the stored value does not match the requested type, the getter\nreturns `nil`.\n\n| Method                                          | Returns                       |\n|-------------------------------------------------|-------------------------------|\n| `get_string(key, contexts = nil)`               | `String` or `nil`             |\n| `get_int(key, contexts = nil)`                  | `Integer` or `nil`            |\n| `get_float(key, contexts = nil)`                | `Float` or `nil`              |\n| `get_bool(key, contexts = nil)`                 | `true`, `false`, or `nil`     |\n| `get_string_list(key, contexts = nil)`          | `Array\u003cString\u003e` or `nil`      |\n| `get_duration(key, contexts = nil)`             | `Float` (seconds) or `nil`    |\n| `get_json(key, contexts = nil)`                 | `Hash`, `Array`, or `nil`     |\n| `enabled?(feature_name, contexts = nil)`        | `true` or `false`             |\n\nExample:\n\n```ruby\nclient.get_string('app.display-name')\nclient.get_int('rate-limit', user: { key: 'user-123' })\nclient.get_float('pricing.multiplier')\nclient.get_bool('flags.new-checkout')\nclient.get_string_list('allowed-regions')\nclient.get_duration('request-timeout')\nclient.get_json('homepage.layout')\nclient.enabled?('beta-feature', user: { key: 'user-123' })\n```\n\n## Dynamic log levels (SemanticLogger)\n\nQuonfig can drive per-class log levels at runtime. Set config keys like\n`log-levels.my_app.foo.bar` to one of `trace`, `debug`, `info`, `warn`, `error`,\n`fatal` and wire the filter into SemanticLogger:\n\n```ruby\nrequire 'quonfig'\nrequire 'semantic_logger'\n\nclient = Quonfig::Client.new(sdk_key: ENV['QUONFIG_BACKEND_SDK_KEY'])\nSemanticLogger.add_appender(io: $stdout, filter: client.semantic_logger_filter)\n```\n\nLookup is exact-match only: logger name `MyApp::Foo::Bar` normalizes to\n`log-levels.my_app.foo.bar`. If no key is set the log is allowed through and\nSemanticLogger's static level decides. There is no hierarchy walk — a value on\n`log-levels.my_app` does not affect `log-levels.my_app.foo.bar`.\n\nPass `key_prefix:` to use a prefix other than `log-levels.`:\n\n```ruby\nclient.semantic_logger_filter(key_prefix: 'debug.')\n```\n\n## Dynamic log levels with stdlib Logger\n\nIf you use Ruby's built-in `::Logger` instead of SemanticLogger, wire the\nformatter returned by `client.stdlib_formatter` into your logger:\n\n```ruby\nrequire 'quonfig'\nrequire 'logger'\n\nclient = Quonfig::Client.new(\n  sdk_key:    ENV['QUONFIG_BACKEND_SDK_KEY'],\n  logger_key: 'log-level.my-app'\n)\n\nlogger = ::Logger.new($stdout)\nlogger.level = ::Logger::DEBUG\nlogger.formatter = client.stdlib_formatter(logger_name: 'MyApp::Services::Auth')\n```\n\nThe formatter asks the client `should_log?(logger_path:, desired_level:)`\nfor every call; lines below the configured level return an empty string\n(which `::Logger` writes as zero bytes, suppressing the line). `logger_name`\nis passed to Quonfig verbatim under `quonfig-sdk-logging.key` so a single\n`log-level.my-app` config can drive per-class overrides via rules like\n`PROP_STARTS_WITH_ONE_OF \"MyApp::Services::\"`.\n\nOmit `logger_name:` to have the formatter fall through to the Logger's\n`progname` at call time:\n\n```ruby\nlogger.formatter = client.stdlib_formatter\nlogger.progname  = 'MyApp::Services::Auth'\n```\n\nIf both are supplied, the explicit `logger_name:` wins.\n\n## Rails integration\n\nThe SDK runs a background SSE thread (and optional polling thread) that you do\nnot want to inherit across a `fork(2)`. Forked threads in the child process\nare dead — the SSE socket is held open by a thread that no longer exists, and\nthe child silently stops receiving live updates.\n\nUse `Quonfig::Client#fork` (or `Quonfig.fork` if you use the module-level\nsingleton) in any process that fork-spawns workers. It returns a fresh client\nconfigured for the child: a new `ConfigStore`, a new SSE subscription, and\nsuppressed telemetry double-counting (`Options#is_fork` is set to `true`).\n\n### Puma (clustered mode)\n\n```ruby\n# config/puma.rb\nbefore_fork do\n  Quonfig.instance.stop          # close the master's SSE before forking\nend\n\non_worker_boot do\n  Quonfig.fork                   # rebuild a fresh client per worker\nend\n```\n\nIf you initialize Quonfig lazily (in a Rails initializer) and run Puma in\nsingle mode (no clustering), no fork hook is needed.\n\n### Sidekiq\n\nSidekiq's parent process forks workers. Wire the same lifecycle:\n\n```ruby\n# config/initializers/quonfig.rb\nQuonfig.init(Quonfig::Options.new(sdk_key: ENV.fetch('QUONFIG_BACKEND_SDK_KEY')))\n\n# config/initializers/sidekiq.rb\nSidekiq.configure_server do |config|\n  config.on(:startup)  { Quonfig.fork if Process.ppid != 1 }\n  config.on(:shutdown) { Quonfig.instance.stop rescue nil }\nend\n```\n\nFor Sidekiq web/CLI processes that don't fork (default `concurrency: 1`),\n`Quonfig.init` in the initializer is sufficient.\n\n### Spring / Bootsnap preloaders\n\nSpring forks the preloader for each command. If your initializer creates a\nQuonfig client at boot, the SSE thread will be inherited dead in every child.\nTwo options:\n\n1. **Recommended:** initialize lazily — wrap `Quonfig.init` so it only runs\n   the first time `Quonfig.instance` is called from a non-preloader process.\n2. **Or:** call `Quonfig.fork` from a `Spring.after_fork` hook.\n\n```ruby\n# config/spring.rb\nSpring.after_fork do\n  Quonfig.fork if defined?(Quonfig) \u0026\u0026 Quonfig.instance_variable_get(:@singleton)\nend\n```\n\n### Code reloading (Zeitwerk, development mode)\n\n`Quonfig::Client` is a long-lived object — keep it out of `app/` (where\nZeitwerk reloads classes on every request) and pin it to a constant set in a\nRails initializer. The client itself is reload-safe because it does not\nreference any application classes; the failure mode to avoid is *creating a\nnew client per request*, which leaks SSE threads and quickly exhausts file\ndescriptors.\n\n```ruby\n# config/initializers/quonfig.rb\n# Quonfig.init is idempotent — a second call warns and returns the existing\n# singleton — so it's safe to wrap in to_prepare for reload-friendliness.\nRails.application.config.to_prepare do\n  Quonfig.init(Quonfig::Options.new(sdk_key: ENV.fetch('QUONFIG_BACKEND_SDK_KEY')))\nend\n```\n\n## Thread safety\n\n`Quonfig::Client` is safe to share across threads. Reads (`get`, `enabled?`,\n`get_*`) and SSE-driven writes to the underlying `ConfigStore` use\n`Concurrent::Map` for per-key atomicity. Eventual consistency across an\nenvelope is intentional: a reader concurrent with envelope application may\nobserve the new value for some keys and the old value for others, then\nconverge once the envelope finishes applying.\n\n`Quonfig.fork` is the only safe way to \"carry\" a client across `Process.fork`\n— do not reuse the parent's client in a child process.\n\n## Documentation\n\nFull documentation, including SPEC, SDK reference, and operational guides, is\navailable at [https://quonfig.com/docs](https://quonfig.com/docs).\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquonfig%2Fsdk-ruby","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquonfig%2Fsdk-ruby","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquonfig%2Fsdk-ruby/lists"}