Some programming languages allow developers to “hack” or extend their internals by overriding existing methods in standard libraries, dynamically attaching new behavior to objects, or modifying classes at runtime.
One of the languages that strongly embraces this flexibility is Ruby.
This ability is often referred to as monkey patching, and while it should be used with caution, it can be extremely powerful in real-world scenarios—especially when dealing with legacy systems or unavailable source code.
Ruby and Runtime Flexibility
Ruby is a highly dynamic, object-oriented language where:
- Classes can be reopened and modified at any time
- Methods can be overridden or extended dynamically
- Behavior can be injected into existing objects or modules
- Even core classes (like String, Array, etc.) can be modified
This makes Ruby particularly well-suited for rapid prototyping, metaprogramming, runtime instrumentation, patching legacy dependencies.
However, this flexibility comes with responsibility: poorly designed patches can introduce hard-to-debug issues.
Example
A simple example of extending a built-in class:
class String
def patch
"---" +self.upcase + "---"
end
end
# rbi
> "aaa".patch
=> "---AAA---"
> "test".patch
=> "---aaa---"
This demonstrates how easily Ruby allows you to modify even core classes like String.
Real-world Example: Patching Redis Connection Pool
I encountered a set of legacy Ruby applications that depended on outdated libraries. These dependencies were no longer available in Git repositories, although prebuilt gems were still stored in an internal artifact repository.
As part of a Redis migration, I needed to identify all polyglot services connecting to Redis instances. The goal was to introduce a CLIENT_NAME for every Redis client, regardless of the programming language used.
So that majority of services followed projects structure +/- similar go-lang stack, but those Ruby legacy services were out of the landscape.
Challenges
- No access to source repositories of dependencies
- No explicit Redis connection URLs
- A proprietary “DIY Redis discovery” mechanism
- Redis connections abstracted behind internal libraries
This made it difficult to instrument Redis clients in a standard way.
Solution: Monkey Patching
Fortunately, Ruby’s monkey patching capabilities provided a way forward.
Even without modifying third-party libraries, I was able to intercept Redis connection creation and inject metadata at runtime.
The idea was simple:
As soon as a Redis connection is established, annotate it with metadata such as service name, Ruby version, and Redis client version.
Original Connection Code (Simplified):
module RedisConfig
class Connection
def self.create_instance!(r_name)
redis = Redis.new(options)
redis
end
end
end
Patched Implementation
I created a module that overrides the create_instance! method and augments it with additional instrumentation:
module ServicePatch
module RedisMetadataPatch
def create_instance!(r_name, &blk)
super(r_name) do |redis|
set_open_api_metadata!(redis, r_name)
blk.call(redis) if blk
end
end
private
def set_open_api_metadata!(redis, r_name)
safe_call(redis, [:client, :setname, 'SERVICE_NAME'], r_name)
safe_call(redis, [:client, :setinfo, 'LIB-NAME', "ruby:#{RUBY_VERSION}"], r_name)
safe_call(redis, [:client, :setinfo, 'LIB-VER', Redis::VERSION], r_name)
end
def safe_call(redis, command, r_name)
redis.call(command)
rescue Redis::BaseError, StandardError => e
warn("[redis metadata] #{r_name} #{command.inspect} failed: #{e.class}: #{e.message}")
nil
end
end
end
RedisConfig::Connection.singleton_class.prepend(ServicePatch::RedisMetadataPatch)
Using prepend ensures that:
- The patched method runs before the original implementation
- super correctly delegates to the original method
- The patch is cleanly layered without modifying original code
Results
After deploying this patch, all Redis clients automatically started reporting metadata.
Here is monitoring from Redis server-side that shows how now these ruby services are instrumenting connection name:
valkey.xxxx.xx.xxxx.xxx.cache.amazonaws.com:6379> monitor
OK
1774951026.839060 [0 xx.xx.95.236:48528] "hello" "3" "setname" "service-api1"
1774951026.839435 [0 xx.xx.95.236:48528] "client" "setname" "service-api1"
1774951026.840134 [0 xx.xx.95.236:48528] "client" "setinfo" "LIB-NAME" "ruby:4.0.1"
1774951026.840142 [0 xx.xx.95.236:48528] "client" "setinfo" "LIB-VER" "5.4.1"
1774951026.840614 [0 xx.xx.95.236:48528] "ping"
1774951031.463576 [0 xx.xx.70.215:58252] "hello" "3" "setname" "service-api2"
1774951031.464538 [0 xx.xx.70.215:58252] "client" "setname" "service-api1"
1774951031.468056 [0 xx.xx.70.215:58252] "client" "setinfo" "LIB-NAME" "ruby:4.0.1"
1774951031.468066 [0 xx.xx.70.215:58252] "client" "setinfo" "LIB-VER" "5.4.1"
1774951031.468728 [0 xx.xx.70.215:58252] "ping"
Observability Gains
Once the instrumentation was in place, I was able to use a custom Redis client scanner to analyze traffic to:
- identify which services are connected to which Redis instances
- track command usage patterns
- detect idle or misbehaving clients
- correlate activity across polyglot systems
Example output:
┌─────────────────────┬──────────────────────┬──────────────────────┬─────────┬───────┬───────┬────────┬────────┬────────┬────────┐
│ Client Addr │ Name │ Lib │ Lib Ver │ Age │ Idle │ GET │ MGET │ SET │ ZRANGE │
├─────────────────────┼──────────────────────┼──────────────────────┼─────────┼───────┼───────┼────────┼────────┼────────┼────────┤
│ xx.xx.226.123:27613 │ service-api1 │ ruby:4.0.1 │ 5.4.1 │ 27740 │ 14 │ 0 │ 2 │ 12 │ 0 │
│ xx.xx.240.240:32031 │ service-api2 │ ruby:4.0.1 │ 5.4.1 │ 89306 │ 1838 │ 0 │ 8 │ 48 │ 0 │
│ xx.xx.240.240:41498 │ service-api3 │ ruby:4.0.1 │ 5.4.1 │ 89306 │ 189 │ 0 │ 13 │ 87 │ 0 │
│ xx.xx.254.221:58628 │ service-api4 │ ruby:4.0.1 │ 5.4.1 │ 10503 │ 64 │ 0 │ 11 │ 72 │ 0 │
│ xx.xx.254.221:9620 │ service-api5 │ ruby:4.0.1 │ 5.4.1 │ 10503 │ 1238 │ 0 │ 9 │ 54 │ 0 │
└─────────────────────┴──────────────────────┴──────────────────────┴─────────┴───────┴───────────────────────────────────────────
Conclusion
This approach allowed me to instrument legacy Ruby applications without modifying their dependencies or internal logic. By leveraging Ruby’s dynamic capabilities, I was able to introduce observability into a previously opaque system.
In environments with legacy constraints, such techniques can turn blockers into manageable engineering problems.
And Ruby is very straightforward language to write code, some ideas from it migrated to kotlin.
Top comments (0)