Monitoring Integration
Scanner includes a monitoring abstraction layer that allows APM (Application Performance Monitoring) integration without changing application code.
Architecture
MonitoringService (facade)
↓
Adapter (base class)
├── NullAdapter (default, no-op)
└── CustomAdapter (implement your own)
The default NullAdapter does nothing — monitoring is opt-in.
Using MonitoringService
# Wrap code in a transaction/span
MonitoringService.transaction("scan_execution", "background") do
run_garak_scan
MonitoringService.set_label(:scan_id, scan.id)
MonitoringService.set_label(:target, scan.target.name)
end
# Set labels on the current transaction
MonitoringService.set_label(:user_id, current_user.id)
MonitoringService.set_labels(environment: "production", version: "1.2.3")
# Get the current trace ID (for log correlation)
trace_id = MonitoringService.current_trace_id
# Check if monitoring is active
if MonitoringService.active?
# monitoring-specific code
end
Adapter Interface
All adapters must implement:
transaction(name, type, &block)
Wrap a block of code in a monitoring span.
name— transaction name (e.g.,"scan_execution")type— transaction type:"background","custom","request"- Returns the result of the block
set_label(key, value)
Set a label/tag on the current transaction. Labels are indexed metadata.
set_labels(labels)
Set multiple labels at once from a hash. Default implementation calls set_label for each pair.
current_trace_id
Returns the current trace ID string, or nil if no active transaction.
trace_context
Returns a hash of environment variables for propagating trace context to child processes.
active?
Returns true if monitoring is enabled and running.
service_name
Returns the service name string.
Creating a Custom Adapter
module Monitoring
class MyApmAdapter < Adapter
def initialize
@enabled = ENV["MY_APM_ENABLED"] == "true"
@service_name = ENV.fetch("MY_APM_SERVICE_NAME", "scanner")
end
def transaction(name, type)
return yield unless @enabled
MyApm.start_transaction(name, type: type) { yield }
end
def set_label(key, value)
return unless @enabled
MyApm.set_tag(key.to_s, value.to_s)
end
def current_trace_id
return nil unless @enabled
MyApm.current_transaction&.trace_id
end
def trace_context
return {} unless @enabled
{ "MY_APM_TRACE_ID" => MyApm.current_transaction.trace_id }
end
def active?
@enabled
end
def service_name
@service_name
end
end
end
Register it in MonitoringService.create_adapter:
def self.create_adapter
case determine_provider
when :my_apm
require_relative "monitoring/my_apm_adapter"
Monitoring::MyApmAdapter.new
else
require_relative "monitoring/null_adapter"
Monitoring::NullAdapter.new
end
end
def self.determine_provider
return :my_apm if ENV["MY_APM_ENABLED"] == "true"
:null
end
Distributed Tracing to garak
Scanner passes trace context to the garak subprocess so traces span both the Ruby and Python processes:
MonitoringService.transaction("garak_scan", "background") do
trace_context = MonitoringService.trace_context
# trace_context is passed to garak as environment variables
run_garak_with_env(trace_context)
end
Testing
# In RSpec: mock the service
allow(MonitoringService).to receive(:transaction).and_yield
allow(MonitoringService).to receive(:set_label)
allow(MonitoringService).to receive(:current_trace_id).and_return("test-trace-id")