Event System vs Monkey-Patching
Subscribe to DSPy.rb events when you need custom metrics or logs. The public event API avoids replacing logger internals or patching module methods.
Identify the Patch Boundary
Replacing logger backends reaches into internals that the public event API already exposes:
Replace a Logger Backend
class EventInterceptorBackend < Dry::Logger::Backends::Stream
def call(entry)
if handler = @event_handlers[entry[:event]]
handler.call(entry)
end
super
end
end
DSPy.configure do |config|
config.logger = Dry.Logger(:dspy) do |logger|
logger.add_backend(EventInterceptorBackend.new(stream: "log/production.log"))
end
end
Subscribe Through the Event API
The event system exposes subscriptions without replacing logger or module internals:
Token Cost Tracking
class TokenCostTracker
def initialize
@costs = Hash.new(0.0)
@subscriptions = []
@subscriptions << DSPy.events.subscribe('llm.*') do |event_name, attributes|
model = attributes['gen_ai.request.model']
input_tokens = attributes['gen_ai.usage.prompt_tokens'] || 0
output_tokens = attributes['gen_ai.usage.completion_tokens'] || 0
cost = calculate_cost(model, input_tokens, output_tokens)
@costs[model] += cost
puts "#{model}: $#{cost.round(4)} (total: $#{@costs[model].round(2)})"
end
end
def unsubscribe
@subscriptions.each { |id| DSPy.events.unsubscribe(id) }
@subscriptions.clear
end
end
tracker = TokenCostTracker.new
# Tracks subscribed llm.* events until tracker.unsubscribe is called.
Rate Limiting
class RateLimiter
def initialize(limit: 100)
@requests = Hash.new(0)
@limit = limit
@subscriptions = []
@subscriptions << DSPy.events.subscribe('llm.generate') do |event_name, attributes|
model = attributes['gen_ai.request.model']
key = "#{model}:#{Time.now.to_i / 60}"
@requests[key] += 1
if @requests[key] > @limit
DSPy.event('rate_limit.exceeded', model: model, count: @requests[key])
end
end
end
def unsubscribe
@subscriptions.each { |id| DSPy.events.unsubscribe(id) }
@subscriptions.clear
end
end
Audit Logging
class AuditLogger
def initialize
@subscriptions = []
@subscriptions << DSPy.events.subscribe('llm.*') do |event_name, attributes|
AuditLog.create!(
event: event_name,
model: attributes['gen_ai.request.model'],
tokens: attributes['gen_ai.usage.total_tokens'],
user_id: Current.user&.id,
timestamp: Time.current
)
end
end
def unsubscribe
@subscriptions.each { |id| DSPy.events.unsubscribe(id) }
@subscriptions.clear
end
end
Compare the Supported Boundaries
Event API properties
- Public entry point:
DSPy.events.subscribe()is explicit and searchable - Subscriber isolation: Subscribers can be tested without patching module internals
- Type Safe: Sorbet T::Struct validation for event structures
- Thread Safe: Built-in concurrency protection
- Error Isolated: Failing listeners don’t break others
- No custom backend: Subscription does not require an application-defined logger backend
Monkey-patch liabilities
- Hidden behavior: Prepend modules are invisible in code
- Testing complexity: Hard to test interceptors in isolation
- Fragility: Breaks when internal APIs change
- Performance overhead: Every call goes through override chain
- Debugging difficulty: Stack traces become confusing
Replace a Logger Patch
Before: replace the backend
module ContextInterceptor
def with_span(operation:, **attributes)
super
end
end
DSPy::Context.singleton_class.prepend(ContextInterceptor)
After: subscribe to the event
class MyTracker
def initialize
@subscriptions = []
@subscriptions << DSPy.events.subscribe('llm.*') { |name, attrs| handle_event(attrs) }
end
def unsubscribe
@subscriptions.each { |id| DSPy.events.unsubscribe(id) }
@subscriptions.clear
end
end
tracker = MyTracker.new
Choose an Interception Boundary
Use the event system
- Token tracking and budget management
- Custom analytics and reporting
- Integration with external services
- Performance monitoring
- User-facing observability features
Patch only an unavailable internal hook
- Modify an internal behavior the event API does not expose.
- Treat the patch as version-coupled application code and test it against upgrades.
Inspect Tested Subscribers
See working implementations:
spec/unit/event_system_spec.rb- Thread safety testsspec/unit/event_subscribers_spec.rb- Subscriber patternsspec/support/event_subscriber_examples.rb- Complete implementationsexamples/event_system_demo.rb- Live demonstration