Instrumentation & Monitoring
Authorization runs on every request — often multiple times per request. As your application grows, understanding how authorization performs becomes important. Action Policy provides built-in instrumentation via ActiveSupport::Notifications, giving you visibility into authorization behavior in production.
The Three Events
Action Policy fires three notification events:
| Event | When it fires |
|---|---|
action_policy.apply_rule | Every time a rule is evaluated (includes cached hits) |
action_policy.authorize | Only when authorize! is called (not allowed_to?) |
action_policy.init | When a policy object is initialized |
Event Payloads
action_policy.apply_rule
ActiveSupport::Notifications.subscribe("action_policy.apply_rule") do |event| data = event.payload # data[:policy] — policy class name (e.g., "ProductPolicy") # data[:rule] — rule name (e.g., :update?) # data[:result] — authorization result object; call .value on it for the boolean # event.duration — time in millisecondsendThis event fires for every rule evaluation — both authorize! and allowed_to? calls, including those nested inside policy rules.
action_policy.authorize
ActiveSupport::Notifications.subscribe("action_policy.authorize") do |event| data = event.payload # data[:policy] — policy class name # data[:rule] — rule name # data[:result] — authorization result object (same payload as apply_rule)endThis event fires only when authorize! is called, not for allowed_to?. The payload is identical to action_policy.apply_rule. It is useful when you want to subscribe specifically to hard authorization checks rather than all rule evaluations.
action_policy.init
ActiveSupport::Notifications.subscribe("action_policy.init") do |event| data = event.payload # data[:policy] — policy class name # event.duration — initialization timeendUse this to verify that memoization is working: if you see many init events for the same policy in a single request, policy objects are being created repeatedly instead of being reused.
Sending Metrics to a Monitoring Service
A common pattern is forwarding instrumentation data to a metrics backend like StatsD, Datadog, or Prometheus. Create an initializer:
# Track timing and result for every rule evaluationActiveSupport::Notifications.subscribe("action_policy.apply_rule") do |event| policy = event.payload[:policy] rule = event.payload[:rule] allowed = event.payload[:result].value
# Send timing metric (StatsD example) StatsD.timing( "auth.rule.duration", event.duration, tags: ["policy:#{policy}", "rule:#{rule}"] )
# Track deny rate StatsD.increment( "auth.rule.#{allowed ? 'allowed' : 'denied'}", tags: ["policy:#{policy}", "rule:#{rule}"] )end
# Track all hard authorization checks (authorize! calls only)ActiveSupport::Notifications.subscribe("action_policy.authorize") do |event| result = event.payload[:result] unless result.value StatsD.increment( "auth.unauthorized", tags: ["policy:#{event.payload[:policy]}", "rule:#{event.payload[:rule]}"] ) endendMonitoring Cache Effectiveness
One of the most valuable things to monitor is your authorization cache hit rate. When caching is working well, the same rule evaluated multiple times in a request should be served from cache.
ActiveSupport::Notifications.subscribe("action_policy.apply_rule") do |event| cached = event.payload[:cached]
StatsD.increment( "auth.cache.#{cached ? 'hit' : 'miss'}", tags: ["policy:#{event.payload[:policy]}"] )endIf you see a low cache hit rate for a policy you expected to be memoized, check:
- Is
ActionPolicy::PerThreadCacheenabled? (It’s disabled in test environments by default) - Is the policy object being shared across calls, or re-instantiated each time?
- Are your
cache_keymethods returning consistent values?
Identifying Slow Rules
Slow authorization rules can add measurable latency to every request. Use apply_rule events to find them:
SLOW_RULE_THRESHOLD_MS = 5
ActiveSupport::Notifications.subscribe("action_policy.apply_rule") do |event| next if event.payload[:cached] # Skip cached hits — they're fast by definition (deprecated key, check your version)
if event.duration > SLOW_RULE_THRESHOLD_MS Rails.logger.warn( "[ActionPolicy] Slow rule: #{event.payload[:policy]}##{event.payload[:rule]} " \ "took #{event.duration.round(2)}ms" ) endendCommon causes of slow rules:
- Database queries inside a rule that aren’t memoized
- N+1 lookups when checking associations
- Missing database indexes on columns used in scope filters
Rails Log Integration
For development, you might want authorization results in your Rails logs:
if Rails.env.development? ActiveSupport::Notifications.subscribe("action_policy.apply_rule") do |event| data = event.payload status = data[:result] ? "ALLOWED" : "DENIED" cached = data[:cached] ? " (cached)" : "" Rails.logger.debug( "[Auth] #{status}: #{data[:policy]}##{data[:rule]}#{cached}" ) endendThis gives you a real-time trace of every authorization decision in development logs.
Disabling Instrumentation
If you’ve measured the overhead and decided instrumentation isn’t worth it for your workload, you can disable it entirely:
config.action_policy.instrumentation_enabled = false