JSON-defined condition evaluator with threshold-based alert engine. Pure Kotlin, no Spring dependency.
You have domain objects (vessels, sensors, assets) and a set of configurable rules that should fire when certain field conditions are met. Instead of hardcoding if/else chains, ruleweave lets you store rules as JSON, compile them explicitly, evaluate at runtime against any context type, and get back structured results with priority, SLA deadlines, and per-condition traces.
- No Spring dependency - pure Kotlin library
- JSON condition lists with AND/OR chaining
- Short-circuit evaluation: AND stops on first false, OR stops on first true
- 10 operators:
EQUALS,NOT_EQUALS,GREATER_THAN,LESS_THAN,GREATER_THAN_OR_EQUALS,LESS_THAN_OR_EQUALS,CONTAINS,IN,NOT_IN,BETWEEN - Generic
FieldResolver<C>interface - plug in any context type (map, entity, DTO) - Template interpolation:
{{fieldName}},{{vessel.id}},{{sensor-status}}in rule names and descriptions - Priority inference from numeric rule weight →
CRITICAL / HIGH / MEDIUM / LOW / INFO - SLA deadlines auto-computed from priority (CRITICAL: 12 h, HIGH: 24 h, MEDIUM: 72 h)
- Explicit compile step via
RuleCompilerwith structuredRuleCompilationErrorper invalid rule EvaluationTrace- per-rule and per-condition debug trace included in everyEvaluationResult- Threshold-based alert engine with severity levels and cooldown configuration
Designed for systems where alert rules are configured through a web UI and evaluated in real time against telemetry data.
| Component | Version |
|---|---|
| Kotlin | 1.9+ |
| Java | 21 |
| Jackson | 2.17+ |
Flow:
RuleCompiler.compile(rules)deserializesconditionsJson→List<Condition>for each rule. Invalid rules produceRuleCompilationErrorentries rather than silently being dropped; callers can inspect or fail-fast on errors.RuleEvaluatorImpltakes aList<CompiledRule>and evaluates on eachevaluate()call.- Conditions are chained left-to-right using each condition's
logicalOperator(AND/OR) with short-circuit semantics. - For each condition,
FieldResolver.resolve(field, context)extracts the value from the context object. - The matching operator is dispatched; numeric comparisons use safe
toDoubleOrNull()coercion - non-numeric values returnfalserather than throwing. - Matched rules produce
RuleActionResultobjects with interpolated titles, priority, and SLA deadlines computed from an injectableClock. - Each evaluation produces a
List<RuleTrace>in the result, one per active rule, with per-conditionConditionTraceentries for debugging. AlertRuleEvaluatorImplevaluatesAlertRulethresholds and createsManagedAlertrecords.
// 1. Implement FieldResolver for your context type
val resolver = FieldResolver<Map<String, Any>> { field, ctx -> ctx[field] }
// 2. Define rules with JSON condition lists
val rule = Rule(
name = "High Speed Alert for vessel {{vesselId}}",
conditionsJson = """[
{"field":"speed","operator":"GREATER_THAN","value":25},
{"field":"zone","operator":"NOT_IN","value":"restricted,anchorage","logicalOperator":"AND"}
]""",
actionsJson = "[]",
priority = 15
)
// 3. Compile - check for errors before evaluating
val compiler = RuleCompiler(objectMapper)
val compilationResult = compiler.compile(listOf(rule))
if (compilationResult.hasErrors) {
compilationResult.errors.forEach { error ->
logger.error { "Rule '${error.ruleName}' failed: ${error.message}" }
}
}
// 4. Evaluate
val conditionEvaluator = ConditionEvaluator(resolver)
val templateRenderer = TemplateRenderer(resolver)
val actionBuilder = ActionBuilder(templateRenderer)
val evaluator = RuleEvaluatorImpl(compilationResult.compiled, conditionEvaluator, actionBuilder)
val result = evaluator.evaluate(entityId, mapOf("speed" to 30.0, "zone" to "open-sea", "vesselId" to "V-42"))
// result.matchedRules == 1
// result.actions.first().title == "High Speed Alert for vessel V-42"
// result.actions.first().actionDeadline == Instant.now(clock) + 24h
// 5. Inspect traces
result.traces.forEach { ruleTrace ->
println("Rule '${ruleTrace.ruleName}' matched=${ruleTrace.matched}")
ruleTrace.conditionTraces.forEach { ct ->
println(" field=${ct.field} op=${ct.operator} expected=${ct.expectedValue} actual=${ct.actualValue} matched=${ct.matched}")
}
}[
{ "field": "speed", "operator": "GREATER_THAN", "value": 20 },
{ "field": "status", "operator": "IN", "value": "active,idle", "logicalOperator": "AND" },
{ "field": "fuel", "operator": "BETWEEN", "value": [10, 30], "logicalOperator": "OR" }
]- The first condition's
logicalOperatoris ignored. - Conditions are evaluated left-to-right;
logicalOperatoron each subsequent condition combines it with the running result. IN/NOT_INaccept a comma-separated string or a JSON array.BETWEENrequires exactly two values:[min, max]inclusive.
The pattern {{key}} supports plain field names, dot-notation, and hyphens:
| Template | Example resolved value |
|---|---|
{{vesselId}} |
V-42 |
{{vessel.id}} |
V-42 |
{{sensor-status}} |
OK |
{{metadata.port.name}} |
Rotterdam |
The FieldResolver receives the full key including dots and hyphens - resolution semantics are up to the caller.
| Operator | Types supported | Notes |
|---|---|---|
EQUALS |
string, numeric | Numeric comparison when both sides are numbers |
NOT_EQUALS |
string, numeric | Numeric comparison when both sides are numbers |
GREATER_THAN |
numeric | Returns false if either side is non-numeric |
LESS_THAN |
numeric | Returns false if either side is non-numeric |
GREATER_THAN_OR_EQUALS |
numeric | Returns false if either side is non-numeric |
LESS_THAN_OR_EQUALS |
numeric | Returns false if either side is non-numeric |
CONTAINS |
string | Substring check |
IN |
string | Comma-separated or JSON array |
NOT_IN |
string | Comma-separated or JSON array |
BETWEEN |
numeric | Inclusive; returns false if non-numeric |
AlertRuleEvaluatorImpl maps AlertRule condition types to threshold comparisons:
| Condition Type | Behavior |
|---|---|
GREATER_THAN |
value > threshold |
LESS_THAN |
value < threshold |
EQUALS |
value == threshold |
NOT_EQUALS |
value != threshold |
OUT_OF_RANGE |
value < threshold OR value > conditionThresholdHigh |
STALE |
Placeholder: value > threshold (see Known Limitations) |
DELTA |
Placeholder: value > threshold (see Known Limitations) |
RATE_OF_CHANGE |
Placeholder: value > threshold (see Known Limitations) |
- Flat condition list, not a tree. Conditions are evaluated as a flat list with AND/OR chaining. Nested grouping (e.g.,
(A AND B) OR (C AND D)) is not supported. actionsJsonis reserved. The field exists onRulebut is not parsed or dispatched by the evaluator. It is stored for future action dispatch integration.- Placeholder alert types.
STALE,DELTA, andRATE_OF_CHANGEcurrently use simple threshold comparison as placeholders. Real staleness requires a last-update timestamp; real delta requires a previous value; real rate-of-change requires time-series data. None of these are currently plumbed through. - No persistence layer. Rules and alert rules are passed as constructor arguments. Storage and loading are left to the caller.
- No cooldown enforcement.
AlertRule.cooldownSecondsis stored but not enforced byAlertRuleEvaluatorImpl.
./gradlew build # compile + test
./gradlew test # tests only
./gradlew clean build # clean rebuild0.1.0-alpha - API is unstable and subject to change.
MIT - see LICENSE

