Ruby
Install the latest version
Reforge.init
context = {
user: {
key: 123,
email: "alice@example.com"
}
team: {
key: 456,
name: "AliceCorp"
}
}
result = Reforge.enabled? "my-first-feature-flag", context
puts "my-first-feature-flag is: #{result}"
Initialize Client
If you set REFORGE_BACKEND_SDK_KEY as an environment variable, initializing the client is as easy as
Reforge.init # reads REFORGE_BACKEND_SDK_KEY env var by default
Rails Applications
Initializing Reforge in your application.rb will allow you to reference dynamic configuration in your environment (e.g. staging.rb) and initializers. This is useful for setting environment-specific config like your redis connection URL.
#application.rb
module MyApplication
class Application < Rails::Application
#...
Reforge.init
end
end
Special Considerations with Forking servers like Puma & Unicorn that use workers
Many ruby web servers fork. In order to work properly we should have a Reforge Client running independently in each fork. You do not need to do this if you are only using threads and not workers. If using SemanticLogger, you will also need to reopen the logger in each fork.
- Puma
- Unicorn
If using workers in Puma, you can initialize inside an on_worker_boot hook in your puma.rb config file.
# puma.rb
on_worker_boot do
Reforge.fork
SemanticLogger.reopen # if you are using SemanticLogger
end
If using workers in Unicorn, you can initialize inside an after_fork hook in your unicorn.rb config file:
# unicorn.rb
after_fork do |server, worker|
Reforge.fork
SemanticLogger.reopen # if you are using SemanticLogger
end
Feature Flags
For boolean flags, you can use the enabled? convenience method:
if Reforge.enabled?("my-first-feature-flag")
# ...
else
# ...
end
Feature flags don't have to return just true or false.
You can get other data types using get:
Reforge.get("ff-with-string")
Reforge.get("ff-with-int")
Context
Feature flags become more powerful when we give the flag evaluation rules more information to work with. We do this by providing context of the current user (and/or team, request, etc.)
Global Context
When initializing the client, you can set a global context that will be used for all evaluations.
Reforge.init(
global_context: {
application: {key: "my.corp.web"},
cpu: {count: 4},
clock: {timezone: "UTC"}
}
)
Global context is the least specific context and will be overridden by more specific context passed in at the time of evaluation.
Thread-local (Request-scoped)
To make the best use of Reforge, we recommend setting context in an around_action in your ApplicationController. Setting this context for the life-cycle of the request means the Reforge logger can be aware of your user/etc and you won't have to explicitly pass context into your .enabled? and .get calls.
# application_controller.rb
class ApplicationController < ActionController::Base
around_action do |_, block|
Reforge.with_context(reforge_context, &block)
end
def reforge_context
{
device: {
mobile: mobile?
# ...
},
}.merge(reforge_user_context)
end
def reforge_user_context
return {} unless current_user
{
key: current_user.tracking_id,
id: current_user.id,
email: current_user.email,
country: current_user.country,
# ...
}
end
end
Just-in-time Context
You can also pass context when evaluating individual flags or config values.
context = {
user: {
id: 123,
key: 'user-123',
subscription_level: 'pro',
email: "alice@example.com"
},
team: {
id: 432,
key: 'team-abc',
},
device: {
key: "abcdef",
mobile: true,
}
}
result = Reforge.enabled?("my-first-feature-flag", context)
puts "my-first-feature-flag is: #{result} for #{context.inspect}"
Dynamic Config
Config values are accessed the same way as feature flag values. You can use enabled? as a convenience for boolean values, and get works for all data types
config_key = "my-first-int-config"
puts "#{config_key} is: #{Reforge.get(config_key)}"
Default Values for Configs
Here we ask for the value of a config named max-jobs-per-second, and we specify 10 as a default value if no value is available.
Reforge.get("max-jobs-per-second", 10) # => returns `10` if no value is available
If we don't provide a default and no value is available, a Reforge::Errors::MissingDefaultError error will be raised.
Reforge.get("max-jobs-per-second") # => raises if no value is available
You can modify this behavior by setting the option on_no_default to Reforge::Options::ON_NO_DEFAULT::RETURN_NIL
Dynamic Log Levels
Reforge supports dynamic log levels with both SemanticLogger and Ruby's stdlib Logger. Choose the integration that best fits your application.
Using SemanticLogger (Recommended)
SemanticLogger is a powerful logging framework that provides structured logging and advanced features. To use dynamic logging with SemanticLogger, add it to your Gemfile and configure your app.
- Ruby
- Rails
# Gemfile
gem "semantic_logger"
require "semantic_logger"
require "reforge"
Reforge.init
SemanticLogger.sync!
SemanticLogger.default_level = :trace # Reforge will take over the filtering
SemanticLogger.add_appender(
io: $stdout,
formatter: :json,
filter: ->(log) { Reforge.instance.log_level_client.semantic_filter(log) },
)
# Gemfile
gem "amazing_print"
gem "rails_semantic_logger"
# config/application.rb
Reforge.init
# config/initializers/logging.rb
SemanticLogger.sync!
SemanticLogger.default_level = :trace # Reforge will take over the filtering
SemanticLogger.add_appender(
io: $stdout,
formatter: Rails.env.development? ? :color : :json,
filter: ->(log) { Reforge.instance.log_level_client.semantic_filter(log) },
)
Please read the Puma/Unicorn notes for special considerations with forking servers.
Using stdlib Logger
If you prefer to use Ruby's standard library Logger, you can integrate Reforge's dynamic log levels using a custom formatter:
require "logger"
require "reforge"
# Initialize Reforge
options = Reforge::Options.new(
logger_key: "log.levels" # Configure your log level config key
)
Reforge.init(options)
# Create a logger with Reforge's dynamic formatter
logger = Logger.new($stdout)
logger.level = Logger::DEBUG # Set to lowest level; Reforge will filter
logger.formatter = Reforge.instance.log_level_client.stdlib_formatter("MyApp")
# Now your logger respects dynamic log levels from Reforge
logger.debug "This will be filtered by Reforge's dynamic config"
logger.info "So will this"
The stdlib_formatter method takes a logger name/path as an argument, which Reforge uses to determine the appropriate log level from your configuration.
Configuration
Create a LOG_LEVEL_V2 config in your Reforge dashboard with key log-levels.default:
# Default to INFO for all loggers
default: INFO
# Set specific packages to DEBUG
rules:
- criteria:
reforge-sdk-logging.logger-path:
starts-with: "MyApp::Services"
value: DEBUG
# Only log errors in noisy third-party library
- criteria:
reforge-sdk-logging.logger-path:
starts-with: "SomeGem"
value: ERROR
You can customize the config key name using the logger_key option. This is useful if you have multiple applications sharing the same Reforge project and want to isolate log level configuration per application:
options = Reforge::Options.new(
logger_key: "myapp.log.levels"
)
Reforge.init(options)
The SDK automatically includes lang: "ruby" in the evaluation context, which you can use in your rules to create Ruby-specific log level configurations:
# Different log levels for Ruby vs other languages
rules:
- criteria:
reforge-sdk-logging.lang: ruby
reforge-sdk-logging.logger-path:
starts-with: "MyApp"
value: DEBUG
- criteria:
reforge-sdk-logging.lang: java
reforge-sdk-logging.logger-path:
starts-with: "com.example"
value: INFO
Targeted Log Levels
You can use rules and segmentation to change your log levels based on the current user/request/device context. This allows you to increase log verbosity for specific users, environments, or conditions without affecting your entire application.
The log level evaluation has access to all context that is available during evaluation, not just the reforge-sdk-logging context. This means you can create rules combining:
- SDK logging context (
reforge-sdk-logging.*) - Logger name and language - Global context - Application name, environment, availability zone, etc.
- Thread-local context - User, team, device, request information from request-scoped context
For example, you can create rules like:
# Enable DEBUG logs only for specific application in staging
rules:
- criteria:
application.key: "myapp"
application.environment: "staging"
reforge-sdk-logging.logger-path:
starts-with: "MyApp"
value: DEBUG
# Enable DEBUG logs for a specific user across all applications
- criteria:
user.email: "developer@example.com"
value: DEBUG
# Lower verbosity in production
- criteria:
application.environment: "production"
value: WARN
This allows you to increase log verbosity for specific users, specific applications, particular environments, or any combination of conditions without affecting your entire system.
How It Works
With either integration, you can now adjust your log levels down to the controller or method level in real-time. This is invaluable for debugging production issues! You can set and tweak log levels on-the-fly in the Reforge web app without redeploying your application.
Telemetry
By default, Reforge uploads telemetry that enables a number of useful features. You can alter or disable this behavior using the following options:
| Name | Description | Default |
|---|---|---|
| collect_evaluation_summaries | Send counts of config/flag evaluation results back to Reforge to view in web app | true |
| collect_logger_counts | Send counts of logger usage back to Reforge to power log-levels configuration screen | true |
| context_upload_mode | Upload either context "shapes" (the names and data types your app uses in Reforge contexts) or periodically send full example contexts | :periodic_example |
If you want to change any of these options, you can pass an options object when initializing the Reforge client.
#application.rb
module MyApplication
class Application < Rails::Application
#...
options = Reforge::Options.new(
collect_evaluation_summaries: true,
collect_logger_counts: true,
context_upload_mode: :periodic_example,
)
Reforge.init(options)
end
end
Debugging
In the rare case that you are trying to debug issues that occur within the library, set env var
REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL = debug
Asset Precompilation in Rails
Developers trying to run rake assets:precompile or rails assets:precompile in CI/CD know the pain of missing environment variables. Reforge can help with this, but you don't want to hardcode your Reforge SDK key in your Dockerfile. What should you do instead?
We recommend using a datafile for assets:precompile. You can generate a datafile for your environment using the Reforge CLI:
reforge download --environment test
This will generate a JSON file (e.g., reforge.test.108.config.json) based on your Reforge project’s test environment. You can check into your repo for use in CI/CD and automated testing.
Now you can use the datafile for assets:precompile:
REFORGE_DATAFILE=reforge.test.108.config.json bundle exec rake assets:precompile
Of course, you can generate a datafile for any environment you like and use it in the same way.
Bootstrap & Stub Client-side JavaScript flags and configs
If you're using JavaScript on the client side, you can use the Reforge Ruby client to bootstrap your client-side flags and configs. This helps you avoid loading states while you wait on an HTTP request to Reforge's evaluation endpoint. You can skip the HTTP request altogether.
With the Frontend SDKs
If you want the power of the JavaScript SDK or React SDK, you can use the Ruby client to bootstrap the page with the evaluated flags and configs for the current user context. Just put this in the DOM (perhaps in your application layout) before you load your Reforge frontend SDK.
<%== Reforge.bootstrap_javascript(context) %>
Things work as they normally would with the frontend SDKs, you'll just skip the HTTP request.
Without the Frontend SDKs
If you don't want to use the frontend SDKs, you can get a global window.reforge object to call get and isEnabled on the client side.
<%= Reforge.generate_javascript_stub(context, callback = nil) %>
This will give you feature flags and config values for your current context. You can provide an optional callback to record experiment exposures or other metrics. No HTTP request or SDK needed!
Testing
Test Setup
You can use a datafile for consistency, reproducibility, and offline testing. See Testing with DataFiles.
If you need to test multiple scenarios that depend on a single config or feature key, you can change the Reforge value using a mock or stub.
Example Test
Imagine we want to test a batches method on our Job class. batches depends on job.batch.size and the value for job.batch.size in our default config file is 3.
We can test how batches performs with different values for job.batch.size by mocking the return value of Reforge.get.
class Job < Array
def batches
slice_size = Reforge.get('job.batch.size')
each_slice(slice_size)
end
end
RSpec.describe Job do
describe '#batches' do
it 'returns batches of jobs' do
jobs = Job.new([1, 2, 3, 4, 5])
expect(jobs.batches.map(&:size)).to eq([3, 2])
allow(Reforge).to receive(:get).with('job.batch.size').and_return(2)
expect(jobs.batches.map(&:size)).to eq([2, 2, 1])
end
end
end
Reference
Client Initialization Options
For more control, you can initialize your client with options. Here are the defaults with explanations.
options = Reforge::Options.new(
sdk_key: ENV['REFORGE_BACKEND_SDK_KEY'],
on_no_default: ON_NO_DEFAULT::RAISE, # options :raise, :warn_and_return_nil,
initialization_timeout_sec: 10, # how long to wait before on_init_failure
on_init_failure: ON_INITIALIZATION_FAILURE::RAISE, # choose to crash or continue with local data only if unable to fetch config data from prefab at startup
datafile: ENV['REFORGE_DATAFILE'] || ENV['PREFAB_DATAFILE'],
logger_key: 'log-levels.default', # the config key to use for dynamic log levels
collect_logger_counts: true, # send counts of logger usage back to Reforge to power log-levels configuration screen
collect_max_paths: DEFAULT_MAX_PATHS,
collect_sync_interval: nil,
context_upload_mode: :periodic_example, # :periodic_example, :shape_only, :none
context_max_size: DEFAULT_MAX_EVAL_SUMMARIES,
collect_evaluation_summaries: true, # send counts of config/flag evaluation results back to Reforge to view in web app
collect_max_evaluation_summaries: DEFAULT_MAX_EVAL_SUMMARIES,
allow_telemetry_in_local_mode: false,
global_context: {}
)
Reforge.init(options)