Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby-version: ['3.1','3.2','3.3','3.4']
ruby-version: ['3.2','3.3','3.4']

steps:
- uses: actions/checkout@v4
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 1.12.0 - 2025-10-31

- Restore log level functionality with LOG_LEVEL_V2 support
- Make SemanticLogger optional - SDK now works with or without it
- Add stdlib Logger support as alternative to SemanticLogger
- Add InternalLogger that automatically uses SemanticLogger or stdlib Logger
- Add `logger_key` initialization option for configuring dynamic log levels
- Add `stdlib_formatter` method for stdlib Logger integration

## 1.11.2 - 2025-10-07

- Address OpenSSL issue with vulnerability to truncation attack
Expand Down
3 changes: 1 addition & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ gem 'uuid'

gem 'activesupport', '>= 4'

gem 'semantic_logger', '!= 4.16.0', require: "semantic_logger/sync"

group :development do
gem 'allocation_stats'
gem 'benchmark-ips'
Expand All @@ -21,6 +19,7 @@ group :development do
end

group :test do
gem 'semantic_logger', '!= 4.16.0', require: "semantic_logger/sync"
gem 'minitest'
gem 'minitest-focus'
gem 'minitest-reporters'
Expand Down
100 changes: 100 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,107 @@ after_fork do |server, worker|
end
```

## Dynamic Log Levels

Reforge supports dynamic log level management for Ruby logging frameworks. This allows you to change log levels in real-time without redeploying your application.

Supported loggers:
- SemanticLogger (optional dependency)
- Ruby stdlib Logger

### Setup with SemanticLogger

Add semantic_logger to your Gemfile:

```ruby
# Gemfile
gem "semantic_logger"
```

### Plain Ruby

```ruby
require "semantic_logger"
require "sdk-reforge"

client = Reforge::Client.new(
sdk_key: ENV['REFORGE_BACKEND_SDK_KEY'],
logger_key: 'log-levels.default' # optional, this is the default
)

SemanticLogger.sync!
SemanticLogger.default_level = :trace # Reforge will handle filtering
SemanticLogger.add_appender(
io: $stdout,
formatter: :json,
filter: client.log_level_client.method(:semantic_filter)
)
```

### With Rails

```ruby
# Gemfile
gem "amazing_print"
gem "rails_semantic_logger"
```

```ruby
# config/application.rb
$reforge_client = Reforge::Client.new # reads REFORGE_BACKEND_SDK_KEY env var

# config/initializers/logging.rb
SemanticLogger.sync!
SemanticLogger.default_level = :trace # Reforge will handle filtering
SemanticLogger.add_appender(
io: $stdout,
formatter: Rails.env.development? ? :color : :json,
filter: $reforge_client.log_level_client.method(:semantic_filter)
)
```

```ruby
# puma.rb
on_worker_boot do
SemanticLogger.reopen
Reforge.fork
end
```

### With Ruby stdlib Logger

If you're using Ruby's standard library Logger, you can use a dynamic formatter:

```ruby
require "logger"
require "sdk-reforge"

client = Reforge::Client.new(
sdk_key: ENV['REFORGE_BACKEND_SDK_KEY'],
logger_key: 'log-levels.default' # optional, this is the default
)

logger = Logger.new($stdout)
logger.level = Logger::DEBUG # Set to most verbose level, Reforge will handle filtering
logger.formatter = client.log_level_client.stdlib_formatter('MyApp')
```

The formatter will check dynamic log levels from Reforge and only output logs that meet the configured threshold.

### Configuration

In Reforge Launch, create a `LOG_LEVEL_V2` config with your desired key (default: `log-levels.default`). The config will be evaluated with the following context:

```ruby
{
"reforge-sdk-logging" => {
"lang" => "ruby",
"logger-path" => "your_app.your_class" # class name converted to lowercase with dots
}
}
```

You can set different log levels for different classes/modules using criteria on the `reforge-sdk-logging.logger-path` property.

## Contributing to reforge sdk for ruby

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.11.2
1.12.0
2 changes: 1 addition & 1 deletion lib/prefab_pb.rb

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions lib/reforge/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ def feature_flag_client
@feature_flag_client ||= Reforge::FeatureFlagClient.new(self)
end

def log_level_client
@log_level_client ||= Reforge::LogLevelClient.new(self)
end

def context_shape_aggregator
return nil if @options.collect_max_shapes <= 0

Expand Down
168 changes: 149 additions & 19 deletions lib/reforge/internal_logger.rb
Original file line number Diff line number Diff line change
@@ -1,43 +1,173 @@
module Reforge
class InternalLogger < SemanticLogger::Logger
# frozen_string_literal: true

module Reforge
# Internal logger for the Reforge SDK
# Uses SemanticLogger if available, falls back to stdlib Logger
class InternalLogger
def initialize(klass)
default_level = ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'] ? ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'].downcase.to_sym : :warn
super(klass, default_level)
@klass = klass
@level_sym = nil # Track the symbol level for consistency

if defined?(SemanticLogger)
@logger = create_semantic_logger
@using_semantic = true
else
@logger = create_stdlib_logger
@using_semantic = false
end

# Track all instances regardless of logger type
instances << self
end

def log(log, message = nil, progname = nil, &block)
return if recurse_check[local_log_id]
recurse_check[local_log_id] = true
begin
super(log, message, progname, &block)
ensure
recurse_check[local_log_id] = false
# Log methods
def trace(message = nil, &block)
log_message(:trace, message, &block)
end

def debug(message = nil, &block)
log_message(:debug, message, &block)
end

def info(message = nil, &block)
log_message(:info, message, &block)
end

def warn(message = nil, &block)
log_message(:warn, message, &block)
end

def error(message = nil, &block)
log_message(:error, message, &block)
end

def fatal(message = nil, &block)
log_message(:fatal, message, &block)
end

def level
if @using_semantic
@logger.level
else
# Return the symbol level we tracked, or map from Logger constant
@level_sym || case @logger.level
when Logger::DEBUG then :debug
when Logger::INFO then :info
when Logger::WARN then :warn
when Logger::ERROR then :error
when Logger::FATAL then :fatal
else :warn
end
end
end

def local_log_id
Thread.current.__id__
def level=(new_level)
if @using_semantic
@logger.level = new_level
else
# Track the symbol level for consistency
@level_sym = new_level

# Map symbol to Logger constant
@logger.level = case new_level
when :trace, :debug then Logger::DEBUG
when :info then Logger::INFO
when :warn then Logger::WARN
when :error then Logger::ERROR
when :fatal then Logger::FATAL
else Logger::WARN
end
end
end

# Our client outputs debug logging,
# but if you aren't using Reforge logging this could be too chatty.
# If you aren't using reforge log filter, only log warn level and above
def self.using_reforge_log_filter!
@@instances.each do |l|
l.level = :trace
@@instances&.each do |logger|
logger.level = :trace
end
end

private

def instances
@@instances ||= []
def create_semantic_logger
default_level = env_log_level || :warn
logger = SemanticLogger::Logger.new(@klass, default_level)

# Wrap to prevent recursion
class << logger
def log(log, message = nil, progname = nil, &block)
return if recurse_check[local_log_id]
recurse_check[local_log_id] = true
begin
super(log, message, progname, &block)
ensure
recurse_check[local_log_id] = false
end
end

def local_log_id
Thread.current.__id__
end

private

def recurse_check
@recurse_check ||= Concurrent::Map.new(initial_capacity: 2)
end
end

logger
end

def create_stdlib_logger
require 'logger'
# When using stdlib Logger (no SemanticLogger), write to $stderr only
# Tests use $logs for SemanticLogger-filtered output, not stdlib Logger
logger = Logger.new($stderr)

# When SemanticLogger is not available, default to :warn to match SemanticLogger behavior
default_level_sym = :warn
@level_sym = env_log_level || default_level_sym

logger.level = case @level_sym
when :trace, :debug then Logger::DEBUG
when :info then Logger::INFO
when :warn then Logger::WARN
when :error then Logger::ERROR
when :fatal then Logger::FATAL
else Logger::WARN
end
logger.progname = @klass.to_s

# Use a custom formatter that mimics SemanticLogger format
# SemanticLogger format: "ClassName -- Message"
# This helps tests that expect SemanticLogger-style output
logger.formatter = proc do |severity, datetime, progname, msg|
"#{progname} -- #{msg}\n"
end

logger
end

def env_log_level
level_str = ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL']
level_str&.downcase&.to_sym
end

def recurse_check
@recurse_check ||=Concurrent::Map.new(initial_capacity: 2)
def log_message(level, message, &block)
if @using_semantic
@logger.send(level, message, &block)
else
# stdlib Logger doesn't have trace
level = :debug if level == :trace
@logger.send(level, message || block&.call)
end
end

def instances
@@instances ||= []
end
end
end
Loading