DMN Event Listeners in Aletyx Enterprise Build of Drools¶
Introduction to DMN Event Listeners¶
DMN event listeners provide a powerful mechanism to observe and react to decision evaluation events in your Kogito applications. These listeners can be used for monitoring, debugging, or extending the behavior of DMN evaluations without modifying the core decision logic.
Understanding DMN Event Types¶
DMN event listeners in Kogito can respond to several types of events during decision evaluation:
- All Events: Triggered before and after the entire DMN model is evaluated
- Decision Events: Triggered before and after each decision in the model is evaluated
- Decision Table Events: Triggered before and after a decision table is evaluated
- Context Entry Events: Triggered before and after context entries are evaluated
The DMNRuntimeEventListener Interface¶
At the core of DMN event listening is the DMNRuntimeEventListener
interface:
package org.kie.dmn.api.core.event;
public interface DMNRuntimeEventListener {
// Model-level events
void beforeEvaluateAll(BeforeEvaluateAllEvent event);
void afterEvaluateAll(AfterEvaluateAllEvent event);
// Decision-level events
void beforeEvaluateDecision(BeforeEvaluateDecisionEvent event);
void afterEvaluateDecision(AfterEvaluateDecisionEvent event);
// Decision table events
void beforeEvaluateDecisionTable(BeforeEvaluateDecisionTableEvent event);
void afterEvaluateDecisionTable(AfterEvaluateDecisionTableEvent event);
// Context entry events
void beforeEvaluateContextEntry(BeforeEvaluateContextEntryEvent event);
void afterEvaluateContextEntry(AfterEvaluateContextEntryEvent event);
}
Implementing a Basic DMN Event Listener¶
Let's start with a basic DMN event listener implementation that logs events as they occur:
package org.kie.kogito.dmn.example.listener;
import org.kie.dmn.api.core.event.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A simple DMN event listener that logs events as they occur
*/
public class LoggingDMNRuntimeEventListener implements DMNRuntimeEventListener {
private static final Logger LOG = LoggerFactory.getLogger(DMNRuntimeEventListener.class);
private final String name;
public LoggingDMNRuntimeEventListener(String name) {
this.name = name;
}
@Override
public void beforeEvaluateDecision(BeforeEvaluateDecisionEvent event) { // (1)
LOG.info("{} - About to evaluate decision: {}",
name, event.getDecision().getName());
}
@Override
public void afterEvaluateDecision(AfterEvaluateDecisionEvent event) { // (2)
LOG.info("{} - Completed evaluation of decision: {}. Result: {}",
name, event.getDecision().getName(), event.getResult().getDecisionResult());
}
@Override
public void beforeEvaluateContextEntry(BeforeEvaluateContextEntryEvent event) {
LOG.info("{} - About to evaluate context entry: {}",
name, event.getContextEntry().getVariable());
}
@Override
public void afterEvaluateContextEntry(AfterEvaluateContextEntryEvent event) {
LOG.info("{} - Completed evaluation of context entry: {}",
name, event.getContextEntry().getVariable());
}
@Override
public void beforeEvaluateDecisionTable(BeforeEvaluateDecisionTableEvent event) {
LOG.info("{} - About to evaluate decision table for decision: {}",
name, event.getDecisionTableName());
}
@Override
public void afterEvaluateDecisionTable(AfterEvaluateDecisionTableEvent event) {
LOG.info("{} - Completed evaluation of decision table for decision: {}",
name, event.getDecisionTableName());
}
@Override
public void beforeEvaluateAll(BeforeEvaluateAllEvent event) { // (3)
LOG.info("{} - About to evaluate DMN model: {}",
name, event.getDecisionModel().getName());
}
@Override
public void afterEvaluateAll(AfterEvaluateAllEvent event) { // (4)
LOG.info("{} - Completed evaluation of DMN model: {}",
name, event.getDecisionModel().getName());
}
}
- Called before evaluating an individual decision in the model
- Called after evaluating an individual decision, contains the decision result
- Called before evaluating the entire DMN model
- Called after evaluating the entire DMN model
Understanding Event Objects¶
Each event type provides specific information about the evaluation context:
BeforeEvaluateAllEvent / AfterEvaluateAllEvent¶
// Model-level events provide access to the entire DMN model
BeforeEvaluateAllEvent event = ...;
// Get the DMN model being evaluated
DMNModel model = event.getDecisionModel();
// Access information about the model
String modelName = model.getName();
String modelNamespace = model.getNamespace();
// AfterEvaluateAllEvent also provides access to the evaluation result
AfterEvaluateAllEvent resultEvent = ...;
DMNResult result = resultEvent.getResult();
BeforeEvaluateDecisionEvent / AfterEvaluateDecisionEvent¶
// Decision events provide information about individual decisions
BeforeEvaluateDecisionEvent event = ...;
// Get the decision being evaluated
DMNDecision decision = event.getDecision();
String decisionName = decision.getName();
DMNDecisionType decisionType = decision.getDecisionType(); // e.g., DECISION_TABLE, CONTEXT, etc.
// AfterEvaluateDecisionEvent provides access to the decision result
AfterEvaluateDecisionEvent resultEvent = ...;
Object decisionResult = resultEvent.getResult().getDecisionResult();
BeforeEvaluateDecisionTableEvent / AfterEvaluateDecisionTableEvent¶
// Decision table events provide information about decision table evaluations
BeforeEvaluateDecisionTableEvent event = ...;
// Get the decision table information
String decisionTableName = event.getDecisionTableName();
// AfterEvaluateDecisionTableEvent provides access to matching rules
AfterEvaluateDecisionTableEvent resultEvent = ...;
List<DMNDecisionTableRule> matchedRules = resultEvent.getMatches(); // (1)
for (DMNDecisionTableRule rule : matchedRules) {
// Process each matched rule
String ruleId = rule.getId();
Map<String, Object> outputValues = rule.getOutputValues(); // Get the rule's output values
}
1. The list of matched rules might be empty if no rules matched during evaluation
### BeforeEvaluateContextEntryEvent / AfterEvaluateContextEntryEvent
```java
// Context entry events provide information about context evaluations
BeforeEvaluateContextEntryEvent event = ...;
// Get the context entry information
DMNContextEntry contextEntry = event.getContextEntry();
String variableName = contextEntry.getVariable();
// AfterEvaluateContextEntryEvent provides the evaluated result
AfterEvaluateContextEntryEvent resultEvent = ...;
Object result = resultEvent.getResult();
Registering DMN Event Listeners¶
Kogito provides two methods to register DMN event listeners:
Method 1: Using ApplicationScoped Annotation¶
Create a listener class and annotate it with @ApplicationScoped
:
package org.kie.kogito.dmn.example.listener;
import jakarta.enterprise.context.ApplicationScoped;
import org.kie.dmn.api.core.event.DMNRuntimeEventListener;
/**
* A DMN event listener that will be automatically registered
* with the DMN runtime
*/
@ApplicationScoped // (1)
public class MonitoringDMNListener implements DMNRuntimeEventListener {
// Implementation of listener methods
@Override
public void beforeEvaluateAll(BeforeEvaluateAllEvent event) {
// ...
}
// Other listener methods...
}
- The
@ApplicationScoped
annotation ensures the listener is discovered and registered automatically
Method 2: Using a Configuration Class¶
Create a configuration class that implements DecisionEventListenerConfig
:
package org.kie.kogito.dmn.example.listener;
import jakarta.enterprise.context.ApplicationScoped;
import org.kie.kogito.dmn.config.CachedDecisionEventListenerConfig;
/**
* Configuration class for registering multiple DMN event listeners
*/
@ApplicationScoped
public class CustomDMNEventListenerConfig extends CachedDecisionEventListenerConfig { // (1)
public CustomDMNEventListenerConfig() {
// Register multiple listeners
register(new LoggingDMNRuntimeEventListener("Audit Listener")); // (2)
register(new LoggingDMNRuntimeEventListener("Performance Listener"));
}
}
- Extending
CachedDecisionEventListenerConfig
provides a convenient way to register listeners - You can register multiple listeners with different configurations
Practical Use Cases for DMN Event Listeners¶
1. Auditing Decision Logic¶
@ApplicationScoped
public class AuditingDMNListener implements DMNRuntimeEventListener {
private static final Logger LOG = LoggerFactory.getLogger(AuditingDMNListener.class);
@Inject
AuditService auditService;
@Override
public void afterEvaluateDecision(AfterEvaluateDecisionEvent event) {
// Extract decision information
String decisionName = event.getDecision().getName();
Object result = event.getResult().getDecisionResult();
// Extract input data for the decision
DMNContext context = event.getResult().getContext();
Map<String, Object> inputs = new HashMap<>();
for (String key : context.getAll().keySet()) {
inputs.put(key, context.get(key));
}
LOG.info("Decision '{}' evaluated with result: {}", decisionName, result);
// Record detailed audit information
auditService.recordDecision(
event.getDecision().getModelNamespace(),
event.getDecision().getModelName(),
decisionName,
inputs,
result,
LocalDateTime.now()
);
}
@Override
public void afterEvaluateAll(AfterEvaluateAllEvent event) {
// Record the complete model evaluation result
DMNResult dmnResult = event.getResult();
// Check for any evaluation messages (warnings, errors)
if (!dmnResult.getMessages().isEmpty()) {
for (DMNMessage message : dmnResult.getMessages()) {
LOG.warn("DMN Message: [{}] {}", message.getLevel(), message.getText());
auditService.recordDMNMessage(
event.getDecisionModel().getNamespace(),
event.getDecisionModel().getName(),
message.getSourceId(),
message.getLevel().toString(),
message.getText()
);
}
}
}
}
2. Performance Monitoring¶
@ApplicationScoped
public class PerformanceDMNListener implements DMNRuntimeEventListener {
private static final Logger LOG = LoggerFactory.getLogger(PerformanceDMNListener.class);
// Store start times for decisions and models
private final Map<String, Long> decisionStartTimes = new ConcurrentHashMap<>();
private final Map<String, Long> modelStartTimes = new ConcurrentHashMap<>();
@Inject
MetricsService metricsService;
@Override
public void beforeEvaluateAll(BeforeEvaluateAllEvent event) {
String modelKey = event.getDecisionModel().getNamespace() + "#" + event.getDecisionModel().getName();
modelStartTimes.put(modelKey, System.currentTimeMillis());
}
@Override
public void afterEvaluateAll(AfterEvaluateAllEvent event) {
String modelKey = event.getDecisionModel().getNamespace() + "#" + event.getDecisionModel().getName();
Long startTime = modelStartTimes.remove(modelKey);
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
LOG.info("DMN model '{}' evaluation took {} ms",
event.getDecisionModel().getName(), duration);
// Record metrics
metricsService.recordModelEvaluationTime(
event.getDecisionModel().getNamespace(),
event.getDecisionModel().getName(),
duration
);
}
}
@Override
public void beforeEvaluateDecision(BeforeEvaluateDecisionEvent event) {
String decisionKey = event.getDecision().getModelNamespace() + "#" +
event.getDecision().getModelName() + "#" +
event.getDecision().getName();
decisionStartTimes.put(decisionKey, System.currentTimeMillis());
}
@Override
public void afterEvaluateDecision(AfterEvaluateDecisionEvent event) {
String decisionKey = event.getDecision().getModelNamespace() + "#" +
event.getDecision().getModelName() + "#" +
event.getDecision().getName();
Long startTime = decisionStartTimes.remove(decisionKey);
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
// Only log if duration exceeds threshold (e.g., 100ms)
if (duration > 100) {
LOG.info("Decision '{}' took {} ms to evaluate (exceeds threshold)",
event.getDecision().getName(), duration);
// Record slow decision metrics
metricsService.recordSlowDecisionEvaluation(
event.getDecision().getModelNamespace(),
event.getDecision().getModelName(),
event.getDecision().getName(),
duration
);
}
}
}
}
3. Error Handling and Validation¶
@ApplicationScoped
public class ValidationDMNListener implements DMNRuntimeEventListener {
private static final Logger LOG = LoggerFactory.getLogger(ValidationDMNListener.class);
@Inject
AlertService alertService;
@Override
public void afterEvaluateAll(AfterEvaluateAllEvent event) {
// Check for evaluation errors
DMNResult result = event.getResult();
if (result.hasErrors()) {
String modelName = event.getDecisionModel().getName();
LOG.error("DMN model '{}' evaluation contains errors:", modelName);
// Process and report all error messages
List<DMNMessage> errorMessages = result.getMessages().stream()
.filter(message -> DMNMessage.Severity.ERROR == message.getLevel())
.collect(Collectors.toList());
for (DMNMessage errorMessage : errorMessages) {
LOG.error(" - [{}] {}", errorMessage.getSourceId(), errorMessage.getText());
// Alert for critical decision errors
alertService.sendErrorAlert(
"DMN_EVALUATION_ERROR",
modelName,
errorMessage.getSourceId(),
errorMessage.getText()
);
}
}
}
@Override
public void afterEvaluateDecision(AfterEvaluateDecisionEvent event) {
// Validate decision results against business rules
String decisionName = event.getDecision().getName();
Object result = event.getResult().getDecisionResult();
// Apply domain-specific validation
if (decisionName.equals("Eligibility Decision")) {
if (result instanceof Map) {
Map<String, Object> eligibility = (Map<String, Object>) result;
// Detect inconsistent results
if (Boolean.TRUE.equals(eligibility.get("isEligible")) &&
"Denied".equals(eligibility.get("status"))) {
LOG.error("Inconsistent eligibility decision: isEligible=true but status=Denied");
alertService.sendErrorAlert(
"DMN_LOGIC_ERROR",
event.getDecision().getModelName(),
decisionName,
"Inconsistent eligibility result detected"
);
}
}
}
}
}
4. Decision Table Analysis¶
@ApplicationScoped
public class DecisionTableAnalysisListener implements DMNRuntimeEventListener {
private static final Logger LOG = LoggerFactory.getLogger(DecisionTableAnalysisListener.class);
@Inject
DecisionAnalyticsService analyticsService;
@Override
public void afterEvaluateDecisionTable(AfterEvaluateDecisionTableEvent event) {
String decisionTableName = event.getDecisionTableName();
List<DMNDecisionTableRule> matchedRules = event.getMatches();
// Log matched rules information
if (matchedRules.isEmpty()) {
LOG.warn("Decision table '{}' had no matching rules", decisionTableName);
// Record analytics for no-match scenario
analyticsService.recordNoMatchScenario(
decisionTableName,
event.getContext().getAll() // The input values that led to no match
);
} else if (matchedRules.size() > 1) {
LOG.info("Decision table '{}' had multiple matching rules: {}",
decisionTableName, matchedRules.size());
// Record analytics for multiple-match scenario
analyticsService.recordMultipleMatchScenario(
decisionTableName,
matchedRules.stream().map(DMNDecisionTableRule::getId).collect(Collectors.toList()),
event.getContext().getAll()
);
} else {
// Single rule matched
DMNDecisionTableRule matchedRule = matchedRules.get(0);
LOG.debug("Decision table '{}' matched rule: {}",
decisionTableName, matchedRule.getId());
// Record rule hit frequency for analytics
analyticsService.recordRuleHit(
decisionTableName,
matchedRule.getId()
);
}
}
}
Advanced Patterns¶
Composable DMN Listeners¶
For complex applications, you might want to create a composite pattern for your listeners:
@ApplicationScoped
public class CompositeDMNListener implements DMNRuntimeEventListener {
private final List<DMNRuntimeEventListener> delegates = new ArrayList<>();
@Inject
public CompositeDMNListener(Instance<DMNRuntimeEventListener> listeners) {
listeners.forEach(listener -> {
// Avoid adding self
if (!(listener instanceof CompositeDMNListener)) {
delegates.add(listener);
}
});
}
// Implement all methods to delegate to child listeners
@Override
public void beforeEvaluateAll(BeforeEvaluateAllEvent event) {
for (DMNRuntimeEventListener listener : delegates) {
try {
listener.beforeEvaluateAll(event);
} catch (Exception e) {
// Log and continue with other listeners
Logger.getLogger(getClass()).error(
"Listener {} failed: {}", listener.getClass().getName(), e.getMessage()
);
}
}
}
// Implement other methods similarly...
}
Contextual DMN Listeners¶
You can create listeners that are aware of the broader application context:
@ApplicationScoped
public class ContextualDMNListener implements DMNRuntimeEventListener {
private static final Logger LOG = LoggerFactory.getLogger(ContextualDMNListener.class);
@Inject
private UserContext userContext; // Current user information
@Inject
private TransactionContext txContext; // Current transaction
@Override
public void beforeEvaluateAll(BeforeEvaluateAllEvent event) {
// Add contextual information to the DMN context
DMNContext dmnContext = event.getContext();
// Add user context information
if (userContext.isAuthenticated()) {
dmnContext.set("currentUser", userContext.getUsername());
dmnContext.set("userRoles", userContext.getRoles());
dmnContext.set("organizationId", userContext.getOrganizationId());
}
// Add transaction context
dmnContext.set("transactionId", txContext.getTransactionId());
dmnContext.set("transactionTimestamp", LocalDateTime.now());
LOG.debug("Enriched DMN context with user and transaction information");
}
@Override
public void afterEvaluateAll(AfterEvaluateAllEvent event) {
// Record decision in the transaction audit log
txContext.addAuditEntry(
"DMN_EVALUATION",
event.getDecisionModel().getName(),
userContext.getUsername(),
event.getResult().getMessages().isEmpty() ? "SUCCESS" : "MESSAGES"
);
}
// Implement other methods as needed...
}
Best Practices for DMN Listeners¶
-
Focus on Separation of Concerns: Each listener should handle one aspect of DMN evaluation (auditing, monitoring, validation, etc.)
-
Handle Exceptions Properly: Listeners should never throw exceptions that could disrupt DMN evaluation
-
Consider Performance Impact: Heavy operations should be moved to asynchronous processing
-
Use Proper Logging: Replace System.out with an appropriate logging framework
-
Make Listeners Configurable: Allow listeners to be enabled/disabled or configured through application properties
-
Test Listeners Thoroughly: Unit test listeners with mock events to ensure they behave correctly
Common Pitfalls to Avoid¶
-
Modifying Input Context: Avoid modifying the DMN context in
beforeEvaluateAll
unless you specifically need to change inputs -
Circular Dependencies: Be careful when registering listeners that depend on each other
-
Heavyweight Operations: Avoid expensive operations in listener methods that could slow down decision evaluation
-
Thread Safety Issues: Ensure listeners are thread-safe as they may be called from multiple threads
Configuration Options¶
In Quarkus applications, you can control DMN listeners through configuration properties:
# Enable/disable DMN listeners (default: true)
quarkus.kogito.decisions.event-listeners.enabled=true
# Configure specific listeners
quarkus.kogito.decisions.event-listeners.audit.enabled=true
quarkus.kogito.decisions.event-listeners.metrics.enabled=false
Conclusion¶
DMN event listeners provide a powerful way to extend, monitor, and analyze decision evaluation in your Kogito applications. By implementing the appropriate listener interfaces and registering them with the runtime, you can gain insights into decision logic, measure performance, validate outcomes, and capture detailed audit trails without modifying your core DMN models.
Start with simple logging listeners and gradually expand to more complex implementations as your understanding of the DMN evaluation lifecycle grows. Remember to keep performance considerations in mind, especially for production deployments with high throughput requirements.