Skip to main content

Observability

The engine accepts an injected observer that receives every evaluation result without blocking the evaluation itself.

Built-in LoggingObserver

from airules import LoggingObserver

router = TicketRouter(observer=LoggingObserver())
router.run(ticket)
# INFO TicketRouter: rule='billing' result=<Team.BILLING: 'billing'>
# WARNING TicketRouter: default fired for subject='Unexpected $9.99 on my statement'

LoggingObserver logs successful rule matches at INFO and default hits at WARNING.

Custom observers

Implement the OutcomeObserver protocol for custom metrics - Prometheus counters, Datadog traces, Slack alerts, or anything else:

from airules import OutcomeObserver, Outcome, KnowledgeEngine

class DefaultRateObserver(OutcomeObserver[Ticket, Team]):
def __init__(self) -> None:
self.total = 0
self.defaults = 0

def observe(
self,
outcome: Outcome[Ticket, Team],
engine: KnowledgeEngine[Ticket, Team],
) -> None:
self.total += 1
if outcome.is_default:
self.defaults += 1

@property
def default_rate(self) -> float:
return self.defaults / self.total if self.total else 0.0

The Outcome object

Every observe call receives an Outcome with:

AttributeTypeDescription
outcome.factFactTypeThe input that was evaluated
outcome.matchedboolWhether any rule fired
outcome.rule_namestr | NoneName of the rule that fired
outcome.resultReturnType | NoneThe return value
outcome.is_defaultboolWhether the @Default rule fired

The default rate metric

The most important health signal is the default rate - the percentage of evaluations that fell through to @Default. A rising default rate means new input patterns are arriving that no rule covers.

  • 10% default rate → rules handle 90% of traffic
  • 40% default rate → rules are missing major categories or predicates are too narrow