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:

EventWhen it fires
action_policy.apply_ruleEvery time a rule is evaluated (includes cached hits)
action_policy.authorizeOnly when authorize! is called (not allowed_to?)
action_policy.initWhen 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 milliseconds
end

This 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)
end

This 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 time
end

Use 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:

config/initializers/action_policy_instrumentation.rb
# Track timing and result for every rule evaluation
ActiveSupport::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]}"]
)
end
end

Monitoring 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]}"]
)
end

If you see a low cache hit rate for a policy you expected to be memoized, check:

  1. Is ActionPolicy::PerThreadCache enabled? (It’s disabled in test environments by default)
  2. Is the policy object being shared across calls, or re-instantiated each time?
  3. Are your cache_key methods 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"
)
end
end

Common 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:

config/initializers/action_policy_logging.rb
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}"
)
end
end

This 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/application.rb
config.action_policy.instrumentation_enabled = false
Powered by WebContainers