this is all of the changes now
This commit is contained in:
21
AGENTS.md
21
AGENTS.md
@@ -35,7 +35,7 @@ attune/
|
|||||||
├── config.{development,test}.yaml # Environment configs
|
├── config.{development,test}.yaml # Environment configs
|
||||||
├── Makefile # Common dev tasks
|
├── Makefile # Common dev tasks
|
||||||
├── crates/ # Rust services
|
├── crates/ # Rust services
|
||||||
│ ├── common/ # Shared library (models, db, repos, mq, config, error)
|
│ ├── common/ # Shared library (models, db, repos, mq, config, error, template_resolver)
|
||||||
│ ├── api/ # REST API service (8080)
|
│ ├── api/ # REST API service (8080)
|
||||||
│ ├── executor/ # Execution orchestration service
|
│ ├── executor/ # Execution orchestration service
|
||||||
│ ├── worker/ # Action execution service (multi-runtime)
|
│ ├── worker/ # Action execution service (multi-runtime)
|
||||||
@@ -243,10 +243,27 @@ Enforcement created → Execution scheduled → Worker executes Action
|
|||||||
- **Available at**: `http://localhost:8080` (dev), `/api-spec/openapi.json` for spec
|
- **Available at**: `http://localhost:8080` (dev), `/api-spec/openapi.json` for spec
|
||||||
|
|
||||||
### Common Library (`crates/common`)
|
### Common Library (`crates/common`)
|
||||||
- **Modules**: `models`, `repositories`, `db`, `config`, `error`, `mq`, `crypto`, `utils`, `workflow`, `pack_registry`
|
- **Modules**: `models`, `repositories`, `db`, `config`, `error`, `mq`, `crypto`, `utils`, `workflow`, `pack_registry`, `template_resolver`
|
||||||
- **Exports**: Commonly used types re-exported from `lib.rs`
|
- **Exports**: Commonly used types re-exported from `lib.rs`
|
||||||
- **Repository Layer**: All DB access goes through repositories in `repositories/`
|
- **Repository Layer**: All DB access goes through repositories in `repositories/`
|
||||||
- **Message Queue**: Abstractions in `mq/` for RabbitMQ communication
|
- **Message Queue**: Abstractions in `mq/` for RabbitMQ communication
|
||||||
|
- **Template Resolver**: Resolves `{{ }}` template variables in rule `action_params` during enforcement creation. Re-exported from `attune_common::{TemplateContext, resolve_templates}`.
|
||||||
|
|
||||||
|
### Template Variable Syntax
|
||||||
|
Rule `action_params` support Jinja2-style `{{ source.path }}` templates resolved at enforcement creation time:
|
||||||
|
|
||||||
|
| Namespace | Example | Description |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| `event.payload.*` | `{{ event.payload.service }}` | Event payload fields |
|
||||||
|
| `event.id` | `{{ event.id }}` | Event database ID |
|
||||||
|
| `event.trigger` | `{{ event.trigger }}` | Trigger ref that generated the event |
|
||||||
|
| `event.created` | `{{ event.created }}` | Event creation timestamp (RFC 3339) |
|
||||||
|
| `pack.config.*` | `{{ pack.config.api_token }}` | Pack configuration values |
|
||||||
|
| `system.*` | `{{ system.timestamp }}` | System variables (timestamp, rule info) |
|
||||||
|
|
||||||
|
- **Implementation**: `crates/common/src/template_resolver.rs` (also re-exported from `attune_sensor::template_resolver`)
|
||||||
|
- **Integration**: `crates/executor/src/event_processor.rs` calls `resolve_templates()` in `create_enforcement()`
|
||||||
|
- **IMPORTANT**: The old `trigger.payload.*` syntax was renamed to `event.payload.*` — the payload data comes from the Event, not the Trigger
|
||||||
|
|
||||||
### Web UI (`web/`)
|
### Web UI (`web/`)
|
||||||
- **Generated Client**: OpenAPI client auto-generated from API spec
|
- **Generated Client**: OpenAPI client auto-generated from API spec
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ attune rule create \
|
|||||||
--pack core \
|
--pack core \
|
||||||
--trigger core.webhook \
|
--trigger core.webhook \
|
||||||
--action core.notify \
|
--action core.notify \
|
||||||
--criteria '{"trigger.payload.severity": "critical"}'
|
--criteria '{"event.payload.severity": "critical"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Delete Rule
|
### Delete Rule
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ pub mod pack_registry;
|
|||||||
pub mod repositories;
|
pub mod repositories;
|
||||||
pub mod runtime_detection;
|
pub mod runtime_detection;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
pub mod template_resolver;
|
||||||
pub mod test_executor;
|
pub mod test_executor;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod workflow;
|
pub mod workflow;
|
||||||
|
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
pub use template_resolver::{resolve_templates, TemplateContext};
|
||||||
|
|
||||||
/// Library version
|
/// Library version
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|||||||
@@ -1,27 +1,44 @@
|
|||||||
//! Template Resolver
|
//! Template Resolver
|
||||||
//!
|
//!
|
||||||
//! Resolves template variables in rule action parameters using context from
|
//! Resolves template variables in rule action parameters using context from
|
||||||
//! trigger payloads, pack configuration, and system variables.
|
//! event payloads, pack configuration, and system variables.
|
||||||
//!
|
//!
|
||||||
//! Supports template syntax: `{{ source.path.to.value }}`
|
//! Supports template syntax: `{{ source.path.to.value }}`
|
||||||
//!
|
//!
|
||||||
//! Example:
|
//! ## Available Template Sources
|
||||||
|
//!
|
||||||
|
//! - `event.payload.*` — Fields from the event payload
|
||||||
|
//! - `event.id` — The event's database ID
|
||||||
|
//! - `event.trigger` — The trigger ref that generated the event
|
||||||
|
//! - `event.created` — The event's creation timestamp
|
||||||
|
//! - `pack.config.*` — Pack configuration values
|
||||||
|
//! - `system.*` — System-provided variables (timestamp, rule info, etc.)
|
||||||
|
//!
|
||||||
|
//! ## Example
|
||||||
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use serde_json::json;
|
//! use serde_json::json;
|
||||||
//! use attune_sensor::template_resolver::{TemplateContext, resolve_templates};
|
//! use attune_common::template_resolver::{TemplateContext, resolve_templates};
|
||||||
|
//!
|
||||||
|
//! let context = TemplateContext::new(
|
||||||
|
//! json!({"service": "api-gateway"}),
|
||||||
|
//! json!({}),
|
||||||
|
//! json!({}),
|
||||||
|
//! )
|
||||||
|
//! .with_event_id(42)
|
||||||
|
//! .with_event_trigger("core.webhook")
|
||||||
|
//! .with_event_created("2026-02-05T10:00:00Z");
|
||||||
//!
|
//!
|
||||||
//! let params = json!({
|
//! let params = json!({
|
||||||
//! "message": "Error in {{ trigger.payload.service }}"
|
//! "message": "Error in {{ event.payload.service }}",
|
||||||
|
//! "trigger": "{{ event.trigger }}",
|
||||||
|
//! "event_id": "{{ event.id }}"
|
||||||
//! });
|
//! });
|
||||||
//!
|
//!
|
||||||
//! let context = TemplateContext {
|
|
||||||
//! trigger_payload: json!({"service": "api-gateway"}),
|
|
||||||
//! pack_config: json!({}),
|
|
||||||
//! system_vars: json!({}),
|
|
||||||
//! };
|
|
||||||
//!
|
|
||||||
//! let resolved = resolve_templates(¶ms, &context).unwrap();
|
//! let resolved = resolve_templates(¶ms, &context).unwrap();
|
||||||
//! assert_eq!(resolved["message"], "Error in api-gateway");
|
//! assert_eq!(resolved["message"], "Error in api-gateway");
|
||||||
|
//! assert_eq!(resolved["trigger"], "core.webhook");
|
||||||
|
//! assert_eq!(resolved["event_id"], 42);
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -30,33 +47,71 @@ use serde_json::Value as JsonValue;
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
/// Template context containing all available data sources
|
/// Template context containing all available data sources for template resolution.
|
||||||
|
///
|
||||||
|
/// The context is structured around three namespaces:
|
||||||
|
/// - `event` — Event data including payload, id, trigger ref, and created timestamp
|
||||||
|
/// - `pack.config` — Pack configuration values
|
||||||
|
/// - `system` — System-provided variables
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TemplateContext {
|
pub struct TemplateContext {
|
||||||
/// Event/trigger payload data
|
/// Event data (payload, id, trigger, created) — accessed as `event.*`
|
||||||
pub trigger_payload: JsonValue,
|
pub event: JsonValue,
|
||||||
/// Pack configuration
|
/// Pack configuration — accessed as `pack.config.*`
|
||||||
pub pack_config: JsonValue,
|
pub pack_config: JsonValue,
|
||||||
/// System-provided variables
|
/// System-provided variables — accessed as `system.*`
|
||||||
pub system_vars: JsonValue,
|
pub system_vars: JsonValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemplateContext {
|
impl TemplateContext {
|
||||||
/// Create a new template context
|
/// Create a new template context with an event payload.
|
||||||
pub fn new(trigger_payload: JsonValue, pack_config: JsonValue, system_vars: JsonValue) -> Self {
|
///
|
||||||
|
/// The payload is nested under `event.payload`. Use builder methods
|
||||||
|
/// to add event metadata (`with_event_id`, `with_event_trigger`, `with_event_created`).
|
||||||
|
pub fn new(event_payload: JsonValue, pack_config: JsonValue, system_vars: JsonValue) -> Self {
|
||||||
|
let event = serde_json::json!({
|
||||||
|
"payload": event_payload,
|
||||||
|
});
|
||||||
Self {
|
Self {
|
||||||
trigger_payload,
|
event,
|
||||||
pack_config,
|
pack_config,
|
||||||
system_vars,
|
system_vars,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a value from the context using a dotted path
|
/// Set the event ID in the context (accessible as `{{ event.id }}`).
|
||||||
|
pub fn with_event_id(mut self, id: i64) -> Self {
|
||||||
|
if let Some(obj) = self.event.as_object_mut() {
|
||||||
|
obj.insert("id".to_string(), serde_json::json!(id));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the trigger ref in the context (accessible as `{{ event.trigger }}`).
|
||||||
|
pub fn with_event_trigger(mut self, trigger_ref: &str) -> Self {
|
||||||
|
if let Some(obj) = self.event.as_object_mut() {
|
||||||
|
obj.insert("trigger".to_string(), serde_json::json!(trigger_ref));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the event created timestamp in the context (accessible as `{{ event.created }}`).
|
||||||
|
pub fn with_event_created(mut self, created: &str) -> Self {
|
||||||
|
if let Some(obj) = self.event.as_object_mut() {
|
||||||
|
obj.insert("created".to_string(), serde_json::json!(created));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a value from the context using a dotted path.
|
||||||
///
|
///
|
||||||
/// Supports paths like:
|
/// Supports paths like:
|
||||||
/// - `trigger.payload.field`
|
/// - `event.payload.field` — event payload data
|
||||||
/// - `pack.config.setting`
|
/// - `event.id` — event ID
|
||||||
/// - `system.timestamp`
|
/// - `event.trigger` — trigger ref
|
||||||
|
/// - `event.created` — creation timestamp
|
||||||
|
/// - `pack.config.setting` — pack configuration
|
||||||
|
/// - `system.timestamp` — system variables
|
||||||
pub fn get_value(&self, path: &str) -> Option<JsonValue> {
|
pub fn get_value(&self, path: &str) -> Option<JsonValue> {
|
||||||
let parts: Vec<&str> = path.split('.').collect();
|
let parts: Vec<&str> = path.split('.').collect();
|
||||||
|
|
||||||
@@ -64,18 +119,12 @@ impl TemplateContext {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the root source
|
// Determine the root source and how many path segments to skip
|
||||||
let root = match parts[0] {
|
let (root, skip_count) = match parts[0] {
|
||||||
"trigger" => {
|
"event" => {
|
||||||
// trigger.payload.* paths
|
// event.* paths navigate directly into the event JSON object
|
||||||
if parts.len() < 2 || parts[1] != "payload" {
|
// e.g. event.id, event.trigger, event.created, event.payload.field
|
||||||
warn!(
|
(&self.event, 1)
|
||||||
"Invalid trigger path: {}, expected 'trigger.payload.*'",
|
|
||||||
path
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
&self.trigger_payload
|
|
||||||
}
|
}
|
||||||
"pack" => {
|
"pack" => {
|
||||||
// pack.config.* paths
|
// pack.config.* paths
|
||||||
@@ -83,22 +132,15 @@ impl TemplateContext {
|
|||||||
warn!("Invalid pack path: {}, expected 'pack.config.*'", path);
|
warn!("Invalid pack path: {}, expected 'pack.config.*'", path);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
&self.pack_config
|
(&self.pack_config, 2)
|
||||||
}
|
}
|
||||||
"system" => &self.system_vars,
|
"system" => (&self.system_vars, 1),
|
||||||
_ => {
|
_ => {
|
||||||
warn!("Unknown template source: {}", parts[0]);
|
warn!("Unknown template source: {}", parts[0]);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigate the path (skip the first 2 parts for trigger/pack, 1 for system)
|
|
||||||
let skip_count = match parts[0] {
|
|
||||||
"trigger" | "pack" => 2,
|
|
||||||
"system" => 1,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
extract_nested_value(root, &parts[skip_count..])
|
extract_nested_value(root, &parts[skip_count..])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +150,7 @@ static TEMPLATE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
|||||||
Regex::new(r"\{\{\s*([^}]+?)\s*\}\}").expect("Failed to compile template regex")
|
Regex::new(r"\{\{\s*([^}]+?)\s*\}\}").expect("Failed to compile template regex")
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Resolve all template variables in a JSON value
|
/// Resolve all template variables in a JSON value.
|
||||||
///
|
///
|
||||||
/// Recursively processes objects and arrays, replacing template strings
|
/// Recursively processes objects and arrays, replacing template strings
|
||||||
/// with values from the context.
|
/// with values from the context.
|
||||||
@@ -132,7 +174,7 @@ pub fn resolve_templates(value: &JsonValue, context: &TemplateContext) -> Result
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve templates in a string value
|
/// Resolve templates in a string value.
|
||||||
///
|
///
|
||||||
/// If the string contains a single template that matches the entire string,
|
/// If the string contains a single template that matches the entire string,
|
||||||
/// returns the value with its original type (preserving numbers, booleans, etc).
|
/// returns the value with its original type (preserving numbers, booleans, etc).
|
||||||
@@ -192,7 +234,7 @@ fn resolve_string_template(s: &str, context: &TemplateContext) -> Result<JsonVal
|
|||||||
Ok(JsonValue::String(result))
|
Ok(JsonValue::String(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract a nested value from JSON using a path
|
/// Extract a nested value from JSON using a path.
|
||||||
fn extract_nested_value(root: &JsonValue, path: &[&str]) -> Option<JsonValue> {
|
fn extract_nested_value(root: &JsonValue, path: &[&str]) -> Option<JsonValue> {
|
||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
return Some(root.clone());
|
return Some(root.clone());
|
||||||
@@ -220,7 +262,7 @@ fn extract_nested_value(root: &JsonValue, path: &[&str]) -> Option<JsonValue> {
|
|||||||
Some(current.clone())
|
Some(current.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a JSON value to a string for interpolation
|
/// Convert a JSON value to a string for interpolation.
|
||||||
fn value_to_string(value: &JsonValue) -> String {
|
fn value_to_string(value: &JsonValue) -> String {
|
||||||
match value {
|
match value {
|
||||||
JsonValue::String(s) => s.clone(),
|
JsonValue::String(s) => s.clone(),
|
||||||
@@ -229,7 +271,7 @@ fn value_to_string(value: &JsonValue) -> String {
|
|||||||
JsonValue::Null => String::new(),
|
JsonValue::Null => String::new(),
|
||||||
JsonValue::Array(_) | JsonValue::Object(_) => {
|
JsonValue::Array(_) | JsonValue::Object(_) => {
|
||||||
// For complex types, serialize as JSON
|
// For complex types, serialize as JSON
|
||||||
serde_json::to_string(value).unwrap_or_else(|_| String::new())
|
serde_json::to_string(value).unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,8 +282,8 @@ mod tests {
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
fn create_test_context() -> TemplateContext {
|
fn create_test_context() -> TemplateContext {
|
||||||
TemplateContext {
|
TemplateContext::new(
|
||||||
trigger_payload: json!({
|
json!({
|
||||||
"service": "api-gateway",
|
"service": "api-gateway",
|
||||||
"message": "Connection timeout",
|
"message": "Connection timeout",
|
||||||
"severity": "critical",
|
"severity": "critical",
|
||||||
@@ -253,29 +295,29 @@ mod tests {
|
|||||||
},
|
},
|
||||||
"tags": ["production", "backend"]
|
"tags": ["production", "backend"]
|
||||||
}),
|
}),
|
||||||
pack_config: json!({
|
json!({
|
||||||
"api_token": "secret123",
|
"api_token": "secret123",
|
||||||
"alert_channel": "#incidents",
|
"alert_channel": "#incidents",
|
||||||
"timeout": 30
|
"timeout": 30
|
||||||
}),
|
}),
|
||||||
system_vars: json!({
|
json!({
|
||||||
"timestamp": "2026-01-17T15:30:00Z",
|
"timestamp": "2026-01-17T15:30:00Z",
|
||||||
"rule": {
|
"rule": {
|
||||||
"id": 42,
|
"id": 42,
|
||||||
"ref": "test.rule"
|
"ref": "test.rule"
|
||||||
},
|
|
||||||
"event": {
|
|
||||||
"id": 123
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}
|
)
|
||||||
|
.with_event_id(123)
|
||||||
|
.with_event_trigger("core.error_event")
|
||||||
|
.with_event_created("2026-01-17T15:30:00Z")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_simple_string_substitution() {
|
fn test_simple_string_substitution() {
|
||||||
let context = create_test_context();
|
let context = create_test_context();
|
||||||
let template = json!({
|
let template = json!({
|
||||||
"message": "Hello {{ trigger.payload.service }}"
|
"message": "Hello {{ event.payload.service }}"
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
@@ -287,12 +329,12 @@ mod tests {
|
|||||||
let context = create_test_context();
|
let context = create_test_context();
|
||||||
|
|
||||||
// Number
|
// Number
|
||||||
let template = json!({"count": "{{ trigger.payload.count }}"});
|
let template = json!({"count": "{{ event.payload.count }}"});
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
assert_eq!(result["count"], 42);
|
assert_eq!(result["count"], 42);
|
||||||
|
|
||||||
// Boolean
|
// Boolean
|
||||||
let template = json!({"enabled": "{{ trigger.payload.enabled }}"});
|
let template = json!({"enabled": "{{ event.payload.enabled }}"});
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
assert_eq!(result["enabled"], true);
|
assert_eq!(result["enabled"], true);
|
||||||
}
|
}
|
||||||
@@ -301,8 +343,8 @@ mod tests {
|
|||||||
fn test_nested_object_access() {
|
fn test_nested_object_access() {
|
||||||
let context = create_test_context();
|
let context = create_test_context();
|
||||||
let template = json!({
|
let template = json!({
|
||||||
"host": "{{ trigger.payload.metadata.host }}",
|
"host": "{{ event.payload.metadata.host }}",
|
||||||
"port": "{{ trigger.payload.metadata.port }}"
|
"port": "{{ event.payload.metadata.port }}"
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
@@ -314,8 +356,8 @@ mod tests {
|
|||||||
fn test_array_access() {
|
fn test_array_access() {
|
||||||
let context = create_test_context();
|
let context = create_test_context();
|
||||||
let template = json!({
|
let template = json!({
|
||||||
"first_tag": "{{ trigger.payload.tags.0 }}",
|
"first_tag": "{{ event.payload.tags.0 }}",
|
||||||
"second_tag": "{{ trigger.payload.tags.1 }}"
|
"second_tag": "{{ event.payload.tags.1 }}"
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
@@ -342,20 +384,65 @@ mod tests {
|
|||||||
let template = json!({
|
let template = json!({
|
||||||
"timestamp": "{{ system.timestamp }}",
|
"timestamp": "{{ system.timestamp }}",
|
||||||
"rule_id": "{{ system.rule.id }}",
|
"rule_id": "{{ system.rule.id }}",
|
||||||
"event_id": "{{ system.event.id }}"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
assert_eq!(result["timestamp"], "2026-01-17T15:30:00Z");
|
assert_eq!(result["timestamp"], "2026-01-17T15:30:00Z");
|
||||||
assert_eq!(result["rule_id"], 42);
|
assert_eq!(result["rule_id"], 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_event_metadata_id() {
|
||||||
|
let context = create_test_context();
|
||||||
|
let template = json!({
|
||||||
|
"event_id": "{{ event.id }}"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
assert_eq!(result["event_id"], 123);
|
assert_eq!(result["event_id"], 123);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_event_metadata_trigger() {
|
||||||
|
let context = create_test_context();
|
||||||
|
let template = json!({
|
||||||
|
"trigger_ref": "{{ event.trigger }}"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
|
assert_eq!(result["trigger_ref"], "core.error_event");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_event_metadata_created() {
|
||||||
|
let context = create_test_context();
|
||||||
|
let template = json!({
|
||||||
|
"created_at": "{{ event.created }}"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
|
assert_eq!(result["created_at"], "2026-01-17T15:30:00Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_event_metadata_in_interpolation() {
|
||||||
|
let context = create_test_context();
|
||||||
|
let template = json!({
|
||||||
|
"summary": "Event {{ event.id }} from {{ event.trigger }} at {{ event.created }}"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result["summary"],
|
||||||
|
"Event 123 from core.error_event at 2026-01-17T15:30:00Z"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_missing_value_returns_null() {
|
fn test_missing_value_returns_null() {
|
||||||
let context = create_test_context();
|
let context = create_test_context();
|
||||||
let template = json!({
|
let template = json!({
|
||||||
"missing": "{{ trigger.payload.nonexistent }}"
|
"missing": "{{ event.payload.nonexistent }}"
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
@@ -366,7 +453,7 @@ mod tests {
|
|||||||
fn test_multiple_templates_in_string() {
|
fn test_multiple_templates_in_string() {
|
||||||
let context = create_test_context();
|
let context = create_test_context();
|
||||||
let template = json!({
|
let template = json!({
|
||||||
"message": "Error in {{ trigger.payload.service }}: {{ trigger.payload.message }}"
|
"message": "Error in {{ event.payload.service }}: {{ event.payload.message }}"
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
@@ -396,11 +483,11 @@ mod tests {
|
|||||||
let context = create_test_context();
|
let context = create_test_context();
|
||||||
let template = json!({
|
let template = json!({
|
||||||
"nested": {
|
"nested": {
|
||||||
"field1": "{{ trigger.payload.service }}",
|
"field1": "{{ event.payload.service }}",
|
||||||
"field2": "{{ pack.config.timeout }}"
|
"field2": "{{ pack.config.timeout }}"
|
||||||
},
|
},
|
||||||
"array": [
|
"array": [
|
||||||
"{{ trigger.payload.severity }}",
|
"{{ event.payload.severity }}",
|
||||||
"static value"
|
"static value"
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -414,14 +501,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty_template_context() {
|
fn test_empty_template_context() {
|
||||||
let context = TemplateContext {
|
let context = TemplateContext::new(json!({}), json!({}), json!({}));
|
||||||
trigger_payload: json!({}),
|
|
||||||
pack_config: json!({}),
|
|
||||||
system_vars: json!({}),
|
|
||||||
};
|
|
||||||
|
|
||||||
let template = json!({
|
let template = json!({
|
||||||
"message": "{{ trigger.payload.missing }}"
|
"message": "{{ event.payload.missing }}"
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
@@ -432,7 +515,7 @@ mod tests {
|
|||||||
fn test_whitespace_in_templates() {
|
fn test_whitespace_in_templates() {
|
||||||
let context = create_test_context();
|
let context = create_test_context();
|
||||||
let template = json!({
|
let template = json!({
|
||||||
"message": "{{ trigger.payload.service }}"
|
"message": "{{ event.payload.service }}"
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = resolve_templates(&template, &context).unwrap();
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
@@ -444,12 +527,14 @@ mod tests {
|
|||||||
let context = create_test_context();
|
let context = create_test_context();
|
||||||
let template = json!({
|
let template = json!({
|
||||||
"channel": "{{ pack.config.alert_channel }}",
|
"channel": "{{ pack.config.alert_channel }}",
|
||||||
"message": "🚨 Error in {{ trigger.payload.service }}: {{ trigger.payload.message }}",
|
"message": "🚨 Error in {{ event.payload.service }}: {{ event.payload.message }}",
|
||||||
"severity": "{{ trigger.payload.severity }}",
|
"severity": "{{ event.payload.severity }}",
|
||||||
"details": {
|
"details": {
|
||||||
"host": "{{ trigger.payload.metadata.host }}",
|
"host": "{{ event.payload.metadata.host }}",
|
||||||
"count": "{{ trigger.payload.count }}",
|
"count": "{{ event.payload.count }}",
|
||||||
"tags": "{{ trigger.payload.tags }}"
|
"tags": "{{ event.payload.tags }}",
|
||||||
|
"event_id": "{{ event.id }}",
|
||||||
|
"trigger": "{{ event.trigger }}"
|
||||||
},
|
},
|
||||||
"timestamp": "{{ system.timestamp }}"
|
"timestamp": "{{ system.timestamp }}"
|
||||||
});
|
});
|
||||||
@@ -463,6 +548,52 @@ mod tests {
|
|||||||
assert_eq!(result["severity"], "critical");
|
assert_eq!(result["severity"], "critical");
|
||||||
assert_eq!(result["details"]["host"], "web-01");
|
assert_eq!(result["details"]["host"], "web-01");
|
||||||
assert_eq!(result["details"]["count"], 42);
|
assert_eq!(result["details"]["count"], 42);
|
||||||
|
assert_eq!(result["details"]["event_id"], 123);
|
||||||
|
assert_eq!(result["details"]["trigger"], "core.error_event");
|
||||||
assert_eq!(result["timestamp"], "2026-01-17T15:30:00Z");
|
assert_eq!(result["timestamp"], "2026-01-17T15:30:00Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_context_without_event_metadata() {
|
||||||
|
// Context with only a payload — no id, trigger, or created
|
||||||
|
let context = TemplateContext::new(
|
||||||
|
json!({"service": "test"}),
|
||||||
|
json!({}),
|
||||||
|
json!({}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let template = json!({
|
||||||
|
"service": "{{ event.payload.service }}",
|
||||||
|
"id": "{{ event.id }}",
|
||||||
|
"trigger": "{{ event.trigger }}"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
|
assert_eq!(result["service"], "test");
|
||||||
|
// Missing metadata returns null
|
||||||
|
assert!(result["id"].is_null());
|
||||||
|
assert!(result["trigger"].is_null());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown_source() {
|
||||||
|
let context = create_test_context();
|
||||||
|
let template = json!({
|
||||||
|
"value": "{{ unknown.field }}"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
|
assert!(result["value"].is_null());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_pack_path() {
|
||||||
|
let context = create_test_context();
|
||||||
|
let template = json!({
|
||||||
|
"value": "{{ pack.invalid.field }}"
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = resolve_templates(&template, &context).unwrap();
|
||||||
|
assert!(result["value"].is_null());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@ use attune_common::{
|
|||||||
rule::RuleRepository,
|
rule::RuleRepository,
|
||||||
Create, FindById, List,
|
Create, FindById, List,
|
||||||
},
|
},
|
||||||
|
template_resolver::{resolve_templates, TemplateContext},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Event processor that handles event-to-rule matching
|
/// Event processor that handles event-to-rule matching
|
||||||
@@ -189,8 +190,8 @@ impl EventProcessor {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| serde_json::Map::new());
|
.unwrap_or_else(|| serde_json::Map::new());
|
||||||
|
|
||||||
// Resolve action parameters (simplified - full template resolution would go here)
|
// Resolve action parameters using the template resolver
|
||||||
let resolved_params = Self::resolve_action_params(&rule.action_params, &payload)?;
|
let resolved_params = Self::resolve_action_params(rule, event, &payload)?;
|
||||||
|
|
||||||
let create_input = CreateEnforcementInput {
|
let create_input = CreateEnforcementInput {
|
||||||
rule: Some(rule.id),
|
rule: Some(rule.id),
|
||||||
@@ -240,10 +241,7 @@ impl EventProcessor {
|
|||||||
let payload = match &event.payload {
|
let payload = match &event.payload {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => {
|
None => {
|
||||||
debug!(
|
debug!("Event {} has no payload, matching by default", event.id);
|
||||||
"Event {} has no payload, matching by default",
|
|
||||||
event.id
|
|
||||||
);
|
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -351,14 +349,43 @@ impl EventProcessor {
|
|||||||
Ok(current)
|
Ok(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve action parameters (simplified - full template resolution would go here)
|
/// Resolve action parameters by applying template variable substitution.
|
||||||
|
///
|
||||||
|
/// Replaces `{{ event.payload.* }}`, `{{ event.id }}`, `{{ event.trigger }}`,
|
||||||
|
/// `{{ event.created }}`, `{{ pack.config.* }}`, and `{{ system.* }}` references
|
||||||
|
/// in the rule's `action_params` with values from the event and context.
|
||||||
fn resolve_action_params(
|
fn resolve_action_params(
|
||||||
action_params: &serde_json::Value,
|
rule: &Rule,
|
||||||
_payload: &serde_json::Value,
|
event: &Event,
|
||||||
|
event_payload: &serde_json::Value,
|
||||||
) -> Result<serde_json::Map<String, serde_json::Value>> {
|
) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||||
// For now, just convert to map if it's an object
|
let action_params = &rule.action_params;
|
||||||
// Full implementation would do template resolution
|
|
||||||
if let Some(obj) = action_params.as_object() {
|
// If there are no action params, return empty
|
||||||
|
if action_params.is_null() || action_params.as_object().map_or(true, |o| o.is_empty()) {
|
||||||
|
return Ok(serde_json::Map::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build template context from the event
|
||||||
|
let context = TemplateContext::new(
|
||||||
|
event_payload.clone(),
|
||||||
|
// TODO: Load pack config from database for pack.config.* resolution
|
||||||
|
serde_json::json!({}),
|
||||||
|
serde_json::json!({
|
||||||
|
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||||
|
"rule": {
|
||||||
|
"id": rule.id,
|
||||||
|
"ref": rule.r#ref,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_event_id(event.id)
|
||||||
|
.with_event_trigger(&event.trigger_ref)
|
||||||
|
.with_event_created(&event.created.to_rfc3339());
|
||||||
|
|
||||||
|
let resolved = resolve_templates(action_params, &context)?;
|
||||||
|
|
||||||
|
if let Some(obj) = resolved.as_object() {
|
||||||
Ok(obj.clone())
|
Ok(obj.clone())
|
||||||
} else {
|
} else {
|
||||||
Ok(serde_json::Map::new())
|
Ok(serde_json::Map::new())
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ pub mod rule_lifecycle_listener;
|
|||||||
pub mod sensor_manager;
|
pub mod sensor_manager;
|
||||||
pub mod sensor_worker_registration;
|
pub mod sensor_worker_registration;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod template_resolver;
|
|
||||||
|
// Re-export template resolver from common crate
|
||||||
|
pub mod template_resolver {
|
||||||
|
pub use attune_common::template_resolver::*;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
pub use rule_lifecycle_listener::RuleLifecycleListener;
|
pub use rule_lifecycle_listener::RuleLifecycleListener;
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ Rules are the core automation logic in Attune that connect triggers to actions.
|
|||||||
},
|
},
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "#alerts",
|
"channel": "#alerts",
|
||||||
"message": "Error in {{ trigger.payload.service }}: {{ trigger.payload.message }}",
|
"message": "Error in {{ event.payload.service }}: {{ event.payload.message }}",
|
||||||
"severity": "{{ trigger.payload.severity }}"
|
"severity": "{{ event.payload.severity }}"
|
||||||
},
|
},
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"created": "2024-01-13T10:00:00Z",
|
"created": "2024-01-13T10:00:00Z",
|
||||||
@@ -64,7 +64,7 @@ Rules are the core automation logic in Attune that connect triggers to actions.
|
|||||||
The `action_params` field supports both static values and dynamic templates:
|
The `action_params` field supports both static values and dynamic templates:
|
||||||
|
|
||||||
- **Static values**: `"channel": "#alerts"`
|
- **Static values**: `"channel": "#alerts"`
|
||||||
- **Dynamic from trigger payload**: `"message": "{{ trigger.payload.message }}"`
|
- **Dynamic from event payload**: `"message": "{{ event.payload.message }}"`
|
||||||
- **Dynamic from pack config**: `"token": "{{ pack.config.api_token }}"`
|
- **Dynamic from pack config**: `"token": "{{ pack.config.api_token }}"`
|
||||||
- **System variables**: `"timestamp": "{{ system.timestamp }}"`
|
- **System variables**: `"timestamp": "{{ system.timestamp }}"`
|
||||||
|
|
||||||
@@ -295,8 +295,8 @@ Create a new rule in the system.
|
|||||||
},
|
},
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "#alerts",
|
"channel": "#alerts",
|
||||||
"message": "Error detected: {{ trigger.payload.message }}",
|
"message": "Error detected: {{ event.payload.message }}",
|
||||||
"severity": "{{ trigger.payload.severity }}"
|
"severity": "{{ event.payload.severity }}"
|
||||||
},
|
},
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
@@ -314,7 +314,7 @@ Create a new rule in the system.
|
|||||||
- `conditions`: JSON Logic conditions for rule evaluation (default: `{}`)
|
- `conditions`: JSON Logic conditions for rule evaluation (default: `{}`)
|
||||||
- `action_params`: Parameters to pass to the action (default: `{}`)
|
- `action_params`: Parameters to pass to the action (default: `{}`)
|
||||||
- Supports static values: `"channel": "#alerts"`
|
- Supports static values: `"channel": "#alerts"`
|
||||||
- Supports dynamic templates: `"message": "{{ trigger.payload.message }}"`
|
- Supports dynamic templates: `"message": "{{ event.payload.message }}"`
|
||||||
- Supports pack config: `"token": "{{ pack.config.api_token }}"`
|
- Supports pack config: `"token": "{{ pack.config.api_token }}"`
|
||||||
- `enabled`: Whether the rule is active (default: `true`)
|
- `enabled`: Whether the rule is active (default: `true`)
|
||||||
|
|
||||||
@@ -341,8 +341,8 @@ Create a new rule in the system.
|
|||||||
},
|
},
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "#alerts",
|
"channel": "#alerts",
|
||||||
"message": "Error detected: {{ trigger.payload.message }}",
|
"message": "Error detected: {{ event.payload.message }}",
|
||||||
"severity": "{{ trigger.payload.severity }}"
|
"severity": "{{ event.payload.severity }}"
|
||||||
},
|
},
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"created": "2024-01-13T10:00:00Z",
|
"created": "2024-01-13T10:00:00Z",
|
||||||
@@ -384,7 +384,7 @@ All fields are optional. Only provided fields will be updated.
|
|||||||
},
|
},
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "#critical-alerts",
|
"channel": "#critical-alerts",
|
||||||
"message": "CRITICAL: {{ trigger.payload.service }} - {{ trigger.payload.message }}",
|
"message": "CRITICAL: {{ event.payload.service }} - {{ event.payload.message }}",
|
||||||
"priority": "high"
|
"priority": "high"
|
||||||
},
|
},
|
||||||
"enabled": false
|
"enabled": false
|
||||||
@@ -416,7 +416,7 @@ All fields are optional. Only provided fields will be updated.
|
|||||||
},
|
},
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "#critical-alerts",
|
"channel": "#critical-alerts",
|
||||||
"message": "CRITICAL: {{ trigger.payload.service }} - {{ trigger.payload.message }}",
|
"message": "CRITICAL: {{ event.payload.service }} - {{ event.payload.message }}",
|
||||||
"priority": "high"
|
"priority": "high"
|
||||||
},
|
},
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ attune rule create \
|
|||||||
--pack core \
|
--pack core \
|
||||||
--trigger core.webhook \
|
--trigger core.webhook \
|
||||||
--action core.notify \
|
--action core.notify \
|
||||||
--criteria '{"trigger.payload.severity": "critical"}'
|
--criteria '{"event.payload.severity": "critical"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Delete Rule
|
#### Delete Rule
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ This document provides practical, copy-paste ready examples of rule parameter ma
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Example 2: Dynamic from Trigger Payload
|
## Example 2: Dynamic from Event Payload
|
||||||
|
|
||||||
**Use Case:** Alert with error details from event
|
**Use Case:** Alert with error details from event
|
||||||
|
|
||||||
@@ -58,9 +58,9 @@ This document provides practical, copy-paste ready examples of rule parameter ma
|
|||||||
"action_ref": "slack.post_message",
|
"action_ref": "slack.post_message",
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "#incidents",
|
"channel": "#incidents",
|
||||||
"message": "🚨 Error in {{ trigger.payload.service }}: {{ trigger.payload.error }}",
|
"message": "🚨 Error in {{ event.payload.service }}: {{ event.payload.error }}",
|
||||||
"severity": "{{ trigger.payload.severity }}",
|
"severity": "{{ event.payload.severity }}",
|
||||||
"timestamp": "{{ trigger.payload.timestamp }}"
|
"timestamp": "{{ event.payload.timestamp }}"
|
||||||
},
|
},
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
@@ -163,8 +163,8 @@ This document provides practical, copy-paste ready examples of rule parameter ma
|
|||||||
"action_params": {
|
"action_params": {
|
||||||
"token": "{{ pack.config.token }}",
|
"token": "{{ pack.config.token }}",
|
||||||
"repo": "{{ pack.config.repo_owner }}/{{ pack.config.repo_name }}",
|
"repo": "{{ pack.config.repo_owner }}/{{ pack.config.repo_name }}",
|
||||||
"title": "[{{ trigger.payload.severity }}] {{ trigger.payload.service }}: {{ trigger.payload.error_message }}",
|
"title": "[{{ event.payload.severity }}] {{ event.payload.service }}: {{ event.payload.error_message }}",
|
||||||
"body": "Error Details:\n\nService: {{ trigger.payload.service }}\nSeverity: {{ trigger.payload.severity }}\n\nStack Trace:\n{{ trigger.payload.stack_trace }}",
|
"body": "Error Details:\n\nService: {{ event.payload.service }}\nSeverity: {{ event.payload.severity }}\n\nStack Trace:\n{{ event.payload.stack_trace }}",
|
||||||
"labels": ["bug", "automated"],
|
"labels": ["bug", "automated"],
|
||||||
"assignees": ["oncall"]
|
"assignees": ["oncall"]
|
||||||
},
|
},
|
||||||
@@ -219,12 +219,12 @@ This document provides practical, copy-paste ready examples of rule parameter ma
|
|||||||
"trigger_ref": "core.user_event",
|
"trigger_ref": "core.user_event",
|
||||||
"action_ref": "audit.log",
|
"action_ref": "audit.log",
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"user_id": "{{ trigger.payload.user.id }}",
|
"user_id": "{{ event.payload.user.id }}",
|
||||||
"user_name": "{{ trigger.payload.user.profile.name }}",
|
"user_name": "{{ event.payload.user.profile.name }}",
|
||||||
"user_email": "{{ trigger.payload.user.profile.email }}",
|
"user_email": "{{ event.payload.user.profile.email }}",
|
||||||
"department": "{{ trigger.payload.user.profile.department }}",
|
"department": "{{ event.payload.user.profile.department }}",
|
||||||
"action": "{{ trigger.payload.action }}",
|
"action": "{{ event.payload.action }}",
|
||||||
"ip_address": "{{ trigger.payload.metadata.ip }}"
|
"ip_address": "{{ event.payload.metadata.ip }}"
|
||||||
},
|
},
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
@@ -271,10 +271,10 @@ This document provides practical, copy-paste ready examples of rule parameter ma
|
|||||||
"action_ref": "slack.post_message",
|
"action_ref": "slack.post_message",
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "#alerts",
|
"channel": "#alerts",
|
||||||
"message": "Primary error: {{ trigger.payload.errors.0 }}",
|
"message": "Primary error: {{ event.payload.errors.0 }}",
|
||||||
"secondary_error": "{{ trigger.payload.errors.1 }}",
|
"secondary_error": "{{ event.payload.errors.1 }}",
|
||||||
"environment": "{{ trigger.payload.tags.0 }}",
|
"environment": "{{ event.payload.tags.0 }}",
|
||||||
"severity": "{{ trigger.payload.tags.1 }}"
|
"severity": "{{ event.payload.tags.1 }}"
|
||||||
},
|
},
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
@@ -377,17 +377,17 @@ This document provides practical, copy-paste ready examples of rule parameter ma
|
|||||||
"routing_key": "{{ pack.config.routing_key }}",
|
"routing_key": "{{ pack.config.routing_key }}",
|
||||||
"event_action": "trigger",
|
"event_action": "trigger",
|
||||||
"payload": {
|
"payload": {
|
||||||
"summary": "{{ trigger.payload.metric_name }} exceeded threshold on {{ trigger.payload.host }}",
|
"summary": "{{ event.payload.metric_name }} exceeded threshold on {{ event.payload.host }}",
|
||||||
"severity": "critical",
|
"severity": "critical",
|
||||||
"source": "{{ trigger.payload.host }}",
|
"source": "{{ event.payload.host }}",
|
||||||
"custom_details": {
|
"custom_details": {
|
||||||
"metric": "{{ trigger.payload.metric_name }}",
|
"metric": "{{ event.payload.metric_name }}",
|
||||||
"current_value": "{{ trigger.payload.current_value }}",
|
"current_value": "{{ event.payload.current_value }}",
|
||||||
"threshold": "{{ trigger.payload.threshold }}",
|
"threshold": "{{ event.payload.threshold }}",
|
||||||
"duration": "{{ trigger.payload.duration_seconds }}s"
|
"duration": "{{ event.payload.duration_seconds }}s"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dedup_key": "{{ trigger.payload.host }}_{{ trigger.payload.metric_name }}"
|
"dedup_key": "{{ event.payload.host }}_{{ event.payload.metric_name }}"
|
||||||
},
|
},
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
@@ -463,27 +463,27 @@ This document provides practical, copy-paste ready examples of rule parameter ma
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"title": "Service",
|
"title": "Service",
|
||||||
"value": "{{ trigger.payload.service }}",
|
"value": "{{ event.payload.service }}",
|
||||||
"short": true
|
"short": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Version",
|
"title": "Version",
|
||||||
"value": "{{ trigger.payload.version }}",
|
"value": "{{ event.payload.version }}",
|
||||||
"short": true
|
"short": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Environment",
|
"title": "Environment",
|
||||||
"value": "{{ trigger.payload.environment }}",
|
"value": "{{ event.payload.environment }}",
|
||||||
"short": true
|
"short": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Deployed By",
|
"title": "Deployed By",
|
||||||
"value": "{{ trigger.payload.deployed_by }}",
|
"value": "{{ event.payload.deployed_by }}",
|
||||||
"short": true
|
"short": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"footer": "Attune Automation",
|
"footer": "Attune Automation",
|
||||||
"ts": "{{ trigger.payload.timestamp }}"
|
"ts": "{{ event.payload.timestamp }}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -515,9 +515,9 @@ This document provides practical, copy-paste ready examples of rule parameter ma
|
|||||||
"trigger_ref": "core.alert_event",
|
"trigger_ref": "core.alert_event",
|
||||||
"action_ref": "slack.post_message",
|
"action_ref": "slack.post_message",
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "{{ trigger.payload.severity | default: 'info' | map: {'critical': '#incidents', 'high': '#alerts', 'medium': '#monitoring', 'low': '#logs'} }}",
|
"channel": "{{ event.payload.severity | default: 'info' | map: {'critical': '#incidents', 'high': '#alerts', 'medium': '#monitoring', 'low': '#logs'} }}",
|
||||||
"message": "{{ trigger.payload.message }}",
|
"message": "{{ event.payload.message }}",
|
||||||
"color": "{{ trigger.payload.severity | map: {'critical': 'danger', 'high': 'warning', 'medium': 'good', 'low': '#cccccc'} }}"
|
"color": "{{ event.payload.severity | map: {'critical': 'danger', 'high': 'warning', 'medium': 'good', 'low': '#cccccc'} }}"
|
||||||
},
|
},
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
@@ -584,12 +584,12 @@ The `config` field should contain the same resolved parameters.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"summary": "Error: {{ trigger.payload.message }}",
|
"summary": "Error: {{ event.payload.message }}",
|
||||||
"details": {
|
"details": {
|
||||||
"service": "{{ trigger.payload.service }}",
|
"service": "{{ event.payload.service }}",
|
||||||
"host": "{{ trigger.payload.host }}",
|
"host": "{{ event.payload.host }}",
|
||||||
"timestamp": "{{ trigger.payload.timestamp }}",
|
"timestamp": "{{ event.payload.timestamp }}",
|
||||||
"stack_trace": "{{ trigger.payload.stack_trace }}"
|
"stack_trace": "{{ event.payload.stack_trace }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -600,11 +600,11 @@ The `config` field should contain the same resolved parameters.
|
|||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"user": {
|
"user": {
|
||||||
"id": "{{ trigger.payload.user.id }}",
|
"id": "{{ event.payload.user.id }}",
|
||||||
"name": "{{ trigger.payload.user.name }}",
|
"name": "{{ event.payload.user.name }}",
|
||||||
"email": "{{ trigger.payload.user.email }}"
|
"email": "{{ event.payload.user.email }}"
|
||||||
},
|
},
|
||||||
"action": "{{ trigger.payload.action_type }}"
|
"action": "{{ event.payload.action_type }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ Update the rule to use event data:
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
UPDATE attune.rule
|
UPDATE attune.rule
|
||||||
SET action_params = '{"message": "Timer fired at {{ trigger.payload.fired_at }}"}'::jsonb
|
SET action_params = '{"message": "Timer fired at {{ event.payload.fired_at }}"}'::jsonb
|
||||||
WHERE ref = 'core.rule.timer_10s_echo';
|
WHERE ref = 'core.rule.timer_10s_echo';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ Potential improvements for the parameter form system:
|
|||||||
1. **Advanced validation**: Support for min/max, pattern matching, custom validators
|
1. **Advanced validation**: Support for min/max, pattern matching, custom validators
|
||||||
2. **Conditional fields**: Show/hide fields based on other field values
|
2. **Conditional fields**: Show/hide fields based on other field values
|
||||||
3. **Field hints**: Helper text, examples, tooltips
|
3. **Field hints**: Helper text, examples, tooltips
|
||||||
4. **Template variables**: Autocomplete for Jinja2 template syntax (e.g., `{{ trigger.payload.* }}`)
|
4. **Template variables**: Autocomplete for Jinja2 template syntax (e.g., `{{ event.payload.* }}`)
|
||||||
5. **Schema versioning**: Handle schema changes across pack versions
|
5. **Schema versioning**: Handle schema changes across pack versions
|
||||||
6. **Array item editing**: Better UX for editing array items individually
|
6. **Array item editing**: Better UX for editing array items individually
|
||||||
7. **Nested objects**: Support for deeply nested object schemas
|
7. **Nested objects**: Support for deeply nested object schemas
|
||||||
|
|||||||
@@ -2,22 +2,20 @@
|
|||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
This document provides a quick overview of what exists and what needs to be implemented for rule parameter mapping.
|
This document tracks the implementation status of rule parameter mapping — the system that resolves `{{ }}` template variables in rule `action_params` before passing them to action executions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ What Already Exists
|
## ✅ Completed
|
||||||
|
|
||||||
### Database Schema
|
### Database Schema
|
||||||
- **Migration:** `migrations/20240103000003_add_rule_action_params.sql`
|
- **Migration:** `migrations/20240103000003_add_rule_action_params.sql`
|
||||||
- **Column:** `attune.rule.action_params` (JSONB, default `{}`)
|
- **Column:** `rule.action_params` (JSONB, default `{}`)
|
||||||
- **Index:** `idx_rule_action_params_gin` (GIN index for efficient querying)
|
- **Index:** `idx_rule_action_params_gin` (GIN index for efficient querying)
|
||||||
- **Status:** ✅ Complete
|
|
||||||
|
|
||||||
### Data Models
|
### Data Models
|
||||||
- **File:** `crates/common/src/models.rs`
|
- **File:** `crates/common/src/models.rs`
|
||||||
- **Struct:** `rule::Rule` has `pub action_params: JsonValue` field
|
- **Struct:** `rule::Rule` has `pub action_params: JsonValue` field
|
||||||
- **Status:** ✅ Complete
|
|
||||||
|
|
||||||
### API Layer
|
### API Layer
|
||||||
- **File:** `crates/api/src/dto/rule.rs`
|
- **File:** `crates/api/src/dto/rule.rs`
|
||||||
@@ -26,232 +24,115 @@ This document provides a quick overview of what exists and what needs to be impl
|
|||||||
- `UpdateRuleRequest.action_params` (optional)
|
- `UpdateRuleRequest.action_params` (optional)
|
||||||
- **Response DTOs:**
|
- **Response DTOs:**
|
||||||
- `RuleResponse.action_params`
|
- `RuleResponse.action_params`
|
||||||
- **Status:** ✅ Complete
|
- `RuleSummary.action_params`
|
||||||
|
|
||||||
### Repository Layer
|
### Repository Layer
|
||||||
- **File:** `crates/common/src/repositories/rule.rs`
|
- **File:** `crates/common/src/repositories/rule.rs`
|
||||||
- **Operations:**
|
- **Operations:** CREATE, UPDATE, and SELECT all handle `action_params`
|
||||||
- `CreateRuleInput.action_params` included in INSERT
|
|
||||||
- `UpdateRuleInput.action_params` handled in UPDATE
|
|
||||||
- All SELECT queries include `action_params` column
|
|
||||||
- **Status:** ✅ Complete
|
|
||||||
|
|
||||||
### API Routes
|
### Template Resolver Module
|
||||||
- **File:** `crates/api/src/routes/rules.rs`
|
- **File:** `crates/common/src/template_resolver.rs`
|
||||||
- **Handlers:**
|
- **Struct:** `TemplateContext` with `event`, `pack_config`, and `system_vars` fields
|
||||||
- `create_rule()` accepts `action_params` from request
|
- **Function:** `resolve_templates()` — recursively resolves `{{ }}` templates in JSON values
|
||||||
- `update_rule()` updates `action_params` if provided
|
- **Re-exported** from `attune_common::template_resolver` and `attune_common::{TemplateContext, resolve_templates}`
|
||||||
- **Status:** ✅ Complete
|
- **Also re-exported** from `attune_sensor::template_resolver` for backward compatibility
|
||||||
|
- **20 unit tests** covering all template features
|
||||||
|
|
||||||
### Data Flow (Static Parameters)
|
### Template Syntax
|
||||||
|
|
||||||
|
**Available Sources:**
|
||||||
|
|
||||||
|
| Namespace | Example | Description |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| `event.payload.*` | `{{ event.payload.service }}` | Event payload data |
|
||||||
|
| `event.id` | `{{ event.id }}` | Event database ID |
|
||||||
|
| `event.trigger` | `{{ event.trigger }}` | Trigger ref that generated the event |
|
||||||
|
| `event.created` | `{{ event.created }}` | Event creation timestamp (RFC 3339) |
|
||||||
|
| `pack.config.*` | `{{ pack.config.api_token }}` | Pack configuration values |
|
||||||
|
| `system.*` | `{{ system.timestamp }}` | System-provided variables |
|
||||||
|
|
||||||
|
### Integration in Executor
|
||||||
|
- **File:** `crates/executor/src/event_processor.rs`
|
||||||
|
- **Method:** `resolve_action_params()` builds a `TemplateContext` from the event and rule, then calls `resolve_templates()`
|
||||||
|
- **Context includes:**
|
||||||
|
- `event.id`, `event.trigger`, `event.created`, `event.payload.*` from the `Event` model
|
||||||
|
- `system.timestamp` (current time), `system.rule.id`, `system.rule.ref`
|
||||||
|
- **Called during:** enforcement creation in `create_enforcement()`
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
```
|
```
|
||||||
Rule.action_params (static JSON)
|
Rule.action_params (templates)
|
||||||
↓
|
↓ resolve_templates() in EventProcessor
|
||||||
Enforcement.config (copied verbatim)
|
Enforcement.config (resolved values)
|
||||||
↓
|
↓
|
||||||
Execution.config (passed through)
|
Execution.config (passed through)
|
||||||
↓
|
↓
|
||||||
Worker (receives as action parameters)
|
Worker (receives as action parameters)
|
||||||
```
|
```
|
||||||
- **Status:** ✅ Working for static values
|
|
||||||
|
### Template Features
|
||||||
|
- ✅ Static values pass through unchanged
|
||||||
|
- ✅ Single-template type preservation (numbers, booleans, objects, arrays)
|
||||||
|
- ✅ String interpolation with multiple templates
|
||||||
|
- ✅ Nested object access via dot notation (`event.payload.metadata.host`)
|
||||||
|
- ✅ Array element access by index (`event.payload.tags.0`)
|
||||||
|
- ✅ Missing values resolve to `null` with warning logged
|
||||||
|
- ✅ Empty/null action_params handled gracefully
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- ✅ `docs/workflows/rule-parameter-mapping.md` — comprehensive user guide
|
||||||
|
- ✅ `docs/examples/rule-parameter-examples.md` — real-world examples
|
||||||
|
- ✅ `docs/api/api-rules.md` — API documentation
|
||||||
|
- ✅ Inline code documentation in `template_resolver.rs`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ❌ What's Missing
|
## 🔄 Partially Implemented
|
||||||
|
|
||||||
### Template Resolution Logic
|
|
||||||
- **Needed:** Parse and resolve `{{ }}` templates in `action_params`
|
|
||||||
- **Location:** `crates/sensor/src/` (new module needed)
|
|
||||||
- **Status:** ❌ Not implemented
|
|
||||||
|
|
||||||
### Template Resolver Module
|
|
||||||
```rust
|
|
||||||
// NEW FILE: crates/sensor/src/template_resolver.rs
|
|
||||||
pub struct TemplateContext {
|
|
||||||
pub trigger_payload: JsonValue,
|
|
||||||
pub pack_config: JsonValue,
|
|
||||||
pub system_vars: JsonValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve_templates(
|
|
||||||
params: &JsonValue,
|
|
||||||
context: &TemplateContext
|
|
||||||
) -> Result<JsonValue> {
|
|
||||||
// Implementation needed
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Status:** ❌ Does not exist
|
|
||||||
|
|
||||||
### Pack Config Loading
|
### Pack Config Loading
|
||||||
- **Needed:** Load pack configuration from database
|
- **Current:** Executor passes empty `{}` for `pack.config` context
|
||||||
- **Current:** Rule matcher doesn't load pack config
|
- **Needed:** Load pack configuration from database before template resolution
|
||||||
- **Required for:** `{{ pack.config.* }}` templates
|
- **Impact:** `{{ pack.config.* }}` templates resolve to `null` until implemented
|
||||||
- **Status:** ❌ Not implemented
|
- **TODO comment** in `event_processor.rs` marks the location
|
||||||
|
|
||||||
### Integration in Rule Matcher
|
|
||||||
- **File:** `crates/sensor/src/rule_matcher.rs`
|
|
||||||
- **Method:** `create_enforcement()`
|
|
||||||
- **Current code (line 309):**
|
|
||||||
```rust
|
|
||||||
let config = Some(&rule.action_params);
|
|
||||||
```
|
|
||||||
- **Needed code:**
|
|
||||||
```rust
|
|
||||||
// Load pack config
|
|
||||||
let pack_config = self.load_pack_config(&rule.pack_ref).await?;
|
|
||||||
|
|
||||||
// Build template context
|
|
||||||
let context = TemplateContext {
|
|
||||||
trigger_payload: event.payload.clone().unwrap_or_default(),
|
|
||||||
pack_config,
|
|
||||||
system_vars: self.build_system_vars(rule, event),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve templates
|
|
||||||
let resolved_params = resolve_templates(&rule.action_params, &context)?;
|
|
||||||
let config = Some(resolved_params);
|
|
||||||
```
|
|
||||||
- **Status:** ❌ Not implemented
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
- **File:** `crates/sensor/src/template_resolver.rs` (tests module)
|
|
||||||
- **Needed tests:**
|
|
||||||
- Simple string substitution
|
|
||||||
- Nested object access
|
|
||||||
- Array element access
|
|
||||||
- Type preservation
|
|
||||||
- Missing value handling
|
|
||||||
- Pack config reference
|
|
||||||
- System variables
|
|
||||||
- Multiple templates in one string
|
|
||||||
- Invalid syntax handling
|
|
||||||
- **Status:** ❌ Not implemented
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
- **Needed:** End-to-end test of template resolution
|
|
||||||
- **Scenario:** Create rule with templates → fire event → verify enforcement has resolved params
|
|
||||||
- **Status:** ❌ Not implemented
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 Implementation Checklist
|
## 📋 Remaining Work
|
||||||
|
|
||||||
### Phase 1: MVP (2-3 days)
|
### Phase 1: Complete Core (Short-term)
|
||||||
|
|
||||||
- [ ] **Create template resolver module**
|
- [ ] **Pack config loading** — Load pack config from database for `{{ pack.config.* }}` resolution
|
||||||
- [ ] Define `TemplateContext` struct
|
- [ ] **Integration tests** — End-to-end test: create rule with templates → fire event → verify enforcement has resolved params
|
||||||
- [ ] Implement `resolve_templates()` function
|
|
||||||
- [ ] Regex pattern matching for `{{ }}`
|
|
||||||
- [ ] JSON path extraction with dot notation
|
|
||||||
- [ ] Type preservation logic
|
|
||||||
- [ ] Error handling for missing values
|
|
||||||
- [ ] Unit tests (9+ test cases)
|
|
||||||
|
|
||||||
- [ ] **Add pack config loading**
|
### Phase 2: Advanced Features (Future)
|
||||||
- [ ] Add method to load pack config from database
|
|
||||||
- [ ] Implement in-memory cache with TTL
|
|
||||||
- [ ] Handle missing pack config gracefully
|
|
||||||
|
|
||||||
- [ ] **Integrate with rule matcher**
|
- [ ] **Default values** — Parse `| default: 'value'` syntax for fallback values
|
||||||
- [ ] Update `create_enforcement()` method
|
- [ ] **Filters** — `upper`, `lower`, `trim`, `date`, `truncate`, `json`
|
||||||
- [ ] Load pack config before resolution
|
- [ ] **Conditional templates** — `{% if event.payload.severity == 'critical' %}...{% endif %}`
|
||||||
- [ ] Build template context
|
- [ ] **Performance** — Skip resolution early if no `{{ }}` patterns detected in action_params
|
||||||
- [ ] Call template resolver
|
|
||||||
- [ ] Handle resolution errors
|
|
||||||
- [ ] Log warnings for missing values
|
|
||||||
|
|
||||||
- [ ] **System variables**
|
|
||||||
- [ ] Build system context (timestamp, rule ID, event ID)
|
|
||||||
- [ ] Document available system variables
|
|
||||||
|
|
||||||
- [ ] **Testing**
|
|
||||||
- [ ] Unit tests for template resolver
|
|
||||||
- [ ] Integration test: end-to-end flow
|
|
||||||
- [ ] Test with missing values
|
|
||||||
- [ ] Test with nested objects
|
|
||||||
- [ ] Test with arrays
|
|
||||||
- [ ] Test performance (benchmark)
|
|
||||||
|
|
||||||
- [ ] **Documentation**
|
|
||||||
- [x] User documentation (`docs/rule-parameter-mapping.md`) ✅
|
|
||||||
- [x] API documentation updates (`docs/api-rules.md`) ✅
|
|
||||||
- [ ] Code documentation (inline comments)
|
|
||||||
- [ ] Update sensor service docs
|
|
||||||
|
|
||||||
### Phase 2: Advanced Features (1-2 days, future)
|
|
||||||
|
|
||||||
- [ ] **Default values**
|
|
||||||
- [ ] Parse `| default: 'value'` syntax
|
|
||||||
- [ ] Apply defaults when value is null/missing
|
|
||||||
- [ ] Unit tests
|
|
||||||
|
|
||||||
- [ ] **Filters**
|
|
||||||
- [ ] `upper` - Convert to uppercase
|
|
||||||
- [ ] `lower` - Convert to lowercase
|
|
||||||
- [ ] `trim` - Remove whitespace
|
|
||||||
- [ ] `date: <format>` - Format timestamp
|
|
||||||
- [ ] `truncate: <length>` - Truncate string
|
|
||||||
- [ ] `json` - Serialize to JSON string
|
|
||||||
- [ ] Unit tests for each filter
|
|
||||||
|
|
||||||
- [ ] **Performance optimization**
|
|
||||||
- [ ] Cache compiled regex patterns
|
|
||||||
- [ ] Skip resolution if no `{{ }}` found
|
|
||||||
- [ ] Parallel template resolution
|
|
||||||
- [ ] Benchmark improvements
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔍 Key Implementation Details
|
## 🔍 Template Example
|
||||||
|
|
||||||
### Current Enforcement Creation (line 306-348)
|
**Input (Rule `action_params`):**
|
||||||
|
|
||||||
```rust
|
|
||||||
async fn create_enforcement(&self, rule: &Rule, event: &Event) -> Result<Id> {
|
|
||||||
let payload = event.payload.clone().unwrap_or_default();
|
|
||||||
let config = Some(&rule.action_params); // ← This line needs to change
|
|
||||||
|
|
||||||
let enforcement_id = sqlx::query_scalar!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO attune.enforcement
|
|
||||||
(rule, rule_ref, trigger_ref, config, event, status, payload, condition, conditions)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
||||||
RETURNING id
|
|
||||||
"#,
|
|
||||||
Some(rule.id),
|
|
||||||
&rule.r#ref,
|
|
||||||
&rule.trigger_ref,
|
|
||||||
config, // ← Resolved params go here
|
|
||||||
Some(event.id),
|
|
||||||
EnforcementStatus::Created as EnforcementStatus,
|
|
||||||
payload,
|
|
||||||
EnforcementCondition::All as EnforcementCondition,
|
|
||||||
&rule.conditions
|
|
||||||
)
|
|
||||||
.fetch_one(&self.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// ... rest of method
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Template Examples
|
|
||||||
|
|
||||||
**Input (Rule):**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"message": "Error in {{ event.payload.service }}: {{ event.payload.message }}",
|
||||||
"message": "Error in {{ trigger.payload.service }}: {{ trigger.payload.message }}",
|
|
||||||
"channel": "{{ pack.config.alert_channel }}",
|
"channel": "{{ pack.config.alert_channel }}",
|
||||||
"severity": "{{ trigger.payload.severity }}"
|
"severity": "{{ event.payload.severity }}",
|
||||||
}
|
"event_id": "{{ event.id }}",
|
||||||
|
"trigger": "{{ event.trigger }}"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Context:**
|
**Context (built from Event + Rule):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"trigger": {
|
"event": {
|
||||||
|
"id": 456,
|
||||||
|
"trigger": "core.error_event",
|
||||||
|
"created": "2026-02-05T10:00:00Z",
|
||||||
"payload": {
|
"payload": {
|
||||||
"service": "api-gateway",
|
"service": "api-gateway",
|
||||||
"message": "Connection timeout",
|
"message": "Connection timeout",
|
||||||
@@ -262,114 +143,30 @@ async fn create_enforcement(&self, rule: &Rule, event: &Event) -> Result<Id> {
|
|||||||
"config": {
|
"config": {
|
||||||
"alert_channel": "#incidents"
|
"alert_channel": "#incidents"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"timestamp": "2026-02-05T10:00:01Z",
|
||||||
|
"rule": { "id": 42, "ref": "alerts.error_notification" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output (Enforcement):**
|
**Output (Enforcement `config`):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"config": {
|
|
||||||
"message": "Error in api-gateway: Connection timeout",
|
"message": "Error in api-gateway: Connection timeout",
|
||||||
"channel": "#incidents",
|
"channel": "#incidents",
|
||||||
"severity": "critical"
|
"severity": "critical",
|
||||||
}
|
"event_id": 456,
|
||||||
|
"trigger": "core.error_event"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 Dependencies
|
## Related Documentation
|
||||||
|
|
||||||
### Existing (Already in Cargo.toml)
|
- [Rule Parameter Mapping Guide](./rule-parameter-mapping.md)
|
||||||
- `serde_json` - JSON manipulation ✅
|
- [Rule Parameter Examples](../examples/rule-parameter-examples.md)
|
||||||
- `regex` - Pattern matching ✅
|
- [Rule Management API](../api/api-rules.md)
|
||||||
- `anyhow` - Error handling ✅
|
- [Executor Service Architecture](../architecture/executor-service.md)
|
||||||
- `sqlx` - Database access ✅
|
|
||||||
|
|
||||||
### New Dependencies
|
|
||||||
- **None required** - Can implement with existing dependencies
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Success Criteria
|
|
||||||
|
|
||||||
- [ ] Static parameters continue to work unchanged
|
|
||||||
- [ ] Can reference `{{ trigger.payload.* }}` fields
|
|
||||||
- [ ] Can reference `{{ pack.config.* }}` fields
|
|
||||||
- [ ] Can reference `{{ system.* }}` variables
|
|
||||||
- [ ] Type preservation (strings, numbers, booleans, objects, arrays)
|
|
||||||
- [ ] Nested object access with dot notation works
|
|
||||||
- [ ] Array element access by index works
|
|
||||||
- [ ] Missing values handled gracefully (null + warning)
|
|
||||||
- [ ] Invalid syntax handled gracefully (literal + error)
|
|
||||||
- [ ] Unit tests pass (90%+ coverage)
|
|
||||||
- [ ] Integration tests pass
|
|
||||||
- [ ] Documentation accurate and complete
|
|
||||||
- [ ] No performance regression (<500µs overhead)
|
|
||||||
- [ ] Backward compatibility maintained (100%)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
|
||||||
|
|
||||||
1. **Read documentation:**
|
|
||||||
- `docs/rule-parameter-mapping.md` - User guide
|
|
||||||
- `work-summary/2026-01-17-parameter-templating.md` - Technical spec
|
|
||||||
|
|
||||||
2. **Review current code:**
|
|
||||||
- `crates/sensor/src/rule_matcher.rs:306-348` - Where to integrate
|
|
||||||
- `crates/common/src/models.rs` - Rule model structure
|
|
||||||
- `migrations/20240103000003_add_rule_action_params.sql` - Schema
|
|
||||||
|
|
||||||
3. **Start implementation:**
|
|
||||||
- Create `crates/sensor/src/template_resolver.rs`
|
|
||||||
- Write unit tests first (TDD approach)
|
|
||||||
- Implement template parsing and resolution
|
|
||||||
- Integrate with rule_matcher
|
|
||||||
- Run integration tests
|
|
||||||
|
|
||||||
4. **Test thoroughly:**
|
|
||||||
- Unit tests for all edge cases
|
|
||||||
- Integration test with real database
|
|
||||||
- Manual testing with example rules
|
|
||||||
- Performance benchmarks
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Related Documentation
|
|
||||||
|
|
||||||
- [Rule Parameter Mapping Guide](./rule-parameter-mapping.md) - Complete user documentation
|
|
||||||
- [Rule Management API](./api-rules.md) - API reference with examples
|
|
||||||
- [Sensor Service Architecture](./sensor-service.md) - Service overview
|
|
||||||
- [Implementation Plan](../work-summary/2026-01-17-parameter-templating.md) - Technical specification
|
|
||||||
- [Session Summary](../work-summary/2026-01-17-session-parameter-mapping.md) - Discovery notes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏷️ Status Summary
|
|
||||||
|
|
||||||
| Component | Status | Notes |
|
|
||||||
|-----------|--------|-------|
|
|
||||||
| Database schema | ✅ Complete | `action_params` column exists |
|
|
||||||
| Data models | ✅ Complete | Rule struct has field |
|
|
||||||
| API DTOs | ✅ Complete | Request/response support |
|
|
||||||
| API routes | ✅ Complete | CRUD operations work |
|
|
||||||
| Repository | ✅ Complete | All queries include field |
|
|
||||||
| Static parameters | ✅ Working | Flow end-to-end |
|
|
||||||
| Template resolver | ❌ Missing | Core implementation needed |
|
|
||||||
| Pack config loading | ❌ Missing | Required for `{{ pack.config }}` |
|
|
||||||
| Integration | ❌ Missing | Need to wire up resolver |
|
|
||||||
| Unit tests | ❌ Missing | Tests for resolver needed |
|
|
||||||
| Integration tests | ❌ Missing | E2E test needed |
|
|
||||||
| Documentation | ✅ Complete | User and tech docs done |
|
|
||||||
|
|
||||||
**Overall Status:** 📝 Documented, ⏳ Implementation Pending
|
|
||||||
|
|
||||||
**Priority:** P1 (High)
|
|
||||||
|
|
||||||
**Estimated Effort:** 2-3 days (MVP), 1-2 days (advanced features)
|
|
||||||
|
|
||||||
**Risk:** Low (backward compatible, well-scoped, clear requirements)
|
|
||||||
|
|
||||||
**Value:** High (unlocks production use cases, user expectation)
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
Rules in Attune can specify parameters to pass to actions when triggered. These parameters can be:
|
Rules in Attune can specify parameters to pass to actions when triggered. These parameters can be:
|
||||||
|
|
||||||
1. **Static values** - Hard-coded values defined in the rule
|
1. **Static values** - Hard-coded values defined in the rule
|
||||||
2. **Dynamic from trigger payload** - Values extracted from the event that triggered the rule
|
2. **Dynamic from event payload** - Values extracted from the event that triggered the rule
|
||||||
3. **Dynamic from pack config** - Values from the pack's configuration
|
3. **Dynamic from pack config** - Values from the pack's configuration
|
||||||
|
|
||||||
This enables flexible parameter passing without hardcoding values or requiring custom code.
|
This enables flexible parameter passing without hardcoding values or requiring custom code.
|
||||||
@@ -27,7 +27,7 @@ Rule `action_params` uses a JSON object where each value can be:
|
|||||||
|
|
||||||
**Available Sources:**
|
**Available Sources:**
|
||||||
|
|
||||||
- `trigger.payload.*` - Data from the event payload
|
- `event.payload.*` - Data from the event payload
|
||||||
- `pack.config.*` - Configuration values from the pack
|
- `pack.config.*` - Configuration values from the pack
|
||||||
- `system.*` - System-provided values (timestamp, execution context)
|
- `system.*` - System-provided values (timestamp, execution context)
|
||||||
|
|
||||||
@@ -55,13 +55,13 @@ When this rule triggers, the action receives exactly these parameters.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dynamic Parameters from Trigger Payload
|
## Dynamic Parameters from Event Payload
|
||||||
|
|
||||||
Extract values from the event that triggered the rule.
|
Extract values from the event that triggered the rule.
|
||||||
|
|
||||||
### Example: Alert with Event Data
|
### Example: Alert with Event Data
|
||||||
|
|
||||||
**Trigger Payload:**
|
**Event Payload:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
@@ -84,10 +84,10 @@ Extract values from the event that triggered the rule.
|
|||||||
"action_ref": "slack.post_message",
|
"action_ref": "slack.post_message",
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "#incidents",
|
"channel": "#incidents",
|
||||||
"message": "Error in {{ trigger.payload.service }}: {{ trigger.payload.message }}",
|
"message": "Error in {{ event.payload.service }}: {{ event.payload.message }}",
|
||||||
"severity": "{{ trigger.payload.severity }}",
|
"severity": "{{ event.payload.severity }}",
|
||||||
"host": "{{ trigger.payload.metadata.host }}",
|
"host": "{{ event.payload.metadata.host }}",
|
||||||
"timestamp": "{{ trigger.payload.timestamp }}"
|
"timestamp": "{{ event.payload.timestamp }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -135,7 +135,7 @@ Use configuration values stored at the pack level (useful for API keys, URLs, et
|
|||||||
"token": "{{ pack.config.api_token }}",
|
"token": "{{ pack.config.api_token }}",
|
||||||
"channel": "{{ pack.config.default_channel }}",
|
"channel": "{{ pack.config.default_channel }}",
|
||||||
"username": "{{ pack.config.bot_name }}",
|
"username": "{{ pack.config.bot_name }}",
|
||||||
"message": "{{ trigger.payload.message }}"
|
"message": "{{ event.payload.message }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -160,8 +160,8 @@ Combine static and dynamic values in the same rule:
|
|||||||
"action_params": {
|
"action_params": {
|
||||||
"repo": "myorg/myrepo",
|
"repo": "myorg/myrepo",
|
||||||
"token": "{{ pack.config.github_token }}",
|
"token": "{{ pack.config.github_token }}",
|
||||||
"title": "Error: {{ trigger.payload.message }}",
|
"title": "Error: {{ event.payload.message }}",
|
||||||
"body": "Service {{ trigger.payload.service }} reported an error at {{ trigger.payload.timestamp }}",
|
"body": "Service {{ event.payload.service }} reported an error at {{ event.payload.timestamp }}",
|
||||||
"labels": ["bug", "automated"],
|
"labels": ["bug", "automated"],
|
||||||
"assignees": ["oncall"]
|
"assignees": ["oncall"]
|
||||||
}
|
}
|
||||||
@@ -177,11 +177,11 @@ Access nested properties using dot notation:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"user_id": "{{ trigger.payload.user.id }}",
|
"user_id": "{{ event.payload.user.id }}",
|
||||||
"user_name": "{{ trigger.payload.user.profile.name }}",
|
"user_name": "{{ event.payload.user.profile.name }}",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ip_address": "{{ trigger.payload.request.client_ip }}",
|
"ip_address": "{{ event.payload.request.client_ip }}",
|
||||||
"user_agent": "{{ trigger.payload.request.headers.user_agent }}"
|
"user_agent": "{{ event.payload.request.headers.user_agent }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,8 +196,8 @@ Access array elements by index:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"first_error": "{{ trigger.payload.errors.0 }}",
|
"first_error": "{{ event.payload.errors.0 }}",
|
||||||
"primary_tag": "{{ trigger.payload.tags.0 }}"
|
"primary_tag": "{{ event.payload.tags.0 }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -211,8 +211,8 @@ Provide default values when the referenced field doesn't exist:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"priority": "{{ trigger.payload.priority | default: 'medium' }}",
|
"priority": "{{ event.payload.priority | default: 'medium' }}",
|
||||||
"assignee": "{{ trigger.payload.assignee | default: 'unassigned' }}"
|
"assignee": "{{ event.payload.assignee | default: 'unassigned' }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -226,10 +226,10 @@ Template values preserve their JSON types:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"count": "{{ trigger.payload.count }}", // Number: 42
|
"count": "{{ event.payload.count }}", // Number: 42
|
||||||
"enabled": "{{ trigger.payload.enabled }}", // Boolean: true
|
"enabled": "{{ event.payload.enabled }}", // Boolean: true
|
||||||
"tags": "{{ trigger.payload.tags }}", // Array: ["a", "b"]
|
"tags": "{{ event.payload.tags }}", // Array: ["a", "b"]
|
||||||
"metadata": "{{ trigger.payload.metadata }}" // Object: {"key": "value"}
|
"metadata": "{{ event.payload.metadata }}" // Object: {"key": "value"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -261,8 +261,8 @@ Embed multiple values in a single string:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"message": "User {{ trigger.payload.user_id }} performed {{ trigger.payload.action }} at {{ system.timestamp }}",
|
"message": "User {{ event.payload.user_id }} performed {{ event.payload.action }} at {{ system.timestamp }}",
|
||||||
"subject": "[{{ trigger.payload.severity | upper }}] {{ trigger.payload.service }} Alert"
|
"subject": "[{{ event.payload.severity | upper }}] {{ event.payload.service }} Alert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -276,10 +276,10 @@ Apply transformations to values:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"uppercase_name": "{{ trigger.payload.name | upper }}",
|
"uppercase_name": "{{ event.payload.name | upper }}",
|
||||||
"lowercase_email": "{{ trigger.payload.email | lower }}",
|
"lowercase_email": "{{ event.payload.email | lower }}",
|
||||||
"formatted_date": "{{ trigger.payload.timestamp | date: '%Y-%m-%d' }}",
|
"formatted_date": "{{ event.payload.timestamp | date: '%Y-%m-%d' }}",
|
||||||
"truncated": "{{ trigger.payload.message | truncate: 100 }}"
|
"truncated": "{{ event.payload.message | truncate: 100 }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -310,19 +310,19 @@ Apply transformations to values:
|
|||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "{{ pack.config.alert_channel }}",
|
"channel": "{{ pack.config.alert_channel }}",
|
||||||
"token": "{{ pack.config.slack_token }}",
|
"token": "{{ pack.config.slack_token }}",
|
||||||
"message": "⚠️ Alert from {{ trigger.payload.source }}: {{ trigger.payload.message }}",
|
"message": "⚠️ Alert from {{ event.payload.source }}: {{ event.payload.message }}",
|
||||||
"attachments": [
|
"attachments": [
|
||||||
{
|
{
|
||||||
"color": "{{ trigger.payload.severity | default: 'warning' }}",
|
"color": "{{ event.payload.severity | default: 'warning' }}",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"title": "Service",
|
"title": "Service",
|
||||||
"value": "{{ trigger.payload.service }}",
|
"value": "{{ event.payload.service }}",
|
||||||
"short": true
|
"short": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Environment",
|
"title": "Environment",
|
||||||
"value": "{{ trigger.payload.environment | default: 'production' }}",
|
"value": "{{ event.payload.environment | default: 'production' }}",
|
||||||
"short": true
|
"short": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -349,7 +349,7 @@ Apply transformations to values:
|
|||||||
"token": "{{ pack.config.jira_token }}"
|
"token": "{{ pack.config.jira_token }}"
|
||||||
},
|
},
|
||||||
"issuetype": "Bug",
|
"issuetype": "Bug",
|
||||||
"summary": "[{{ trigger.payload.severity }}] {{ trigger.payload.service }}: {{ trigger.payload.message }}",
|
"summary": "[{{ event.payload.severity }}] {{ event.payload.service }}: {{ event.payload.message }}",
|
||||||
"description": {
|
"description": {
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
"content": [
|
"content": [
|
||||||
@@ -358,14 +358,14 @@ Apply transformations to values:
|
|||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "Error Details:\n\nService: {{ trigger.payload.service }}\nHost: {{ trigger.payload.host }}\nTimestamp: {{ trigger.payload.timestamp }}\n\nStack Trace:\n{{ trigger.payload.stack_trace }}"
|
"text": "Error Details:\n\nService: {{ event.payload.service }}\nHost: {{ event.payload.host }}\nTimestamp: {{ event.payload.timestamp }}\n\nStack Trace:\n{{ event.payload.stack_trace }}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"priority": "{{ trigger.payload.priority | default: 'Medium' }}",
|
"priority": "{{ event.payload.priority | default: 'Medium' }}",
|
||||||
"labels": ["automated", "{{ trigger.payload.service }}"]
|
"labels": ["automated", "{{ event.payload.service }}"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -382,17 +382,17 @@ Apply transformations to values:
|
|||||||
"routing_key": "{{ pack.config.pagerduty_routing_key }}",
|
"routing_key": "{{ pack.config.pagerduty_routing_key }}",
|
||||||
"event_action": "trigger",
|
"event_action": "trigger",
|
||||||
"payload": {
|
"payload": {
|
||||||
"summary": "{{ trigger.payload.metric_name }} exceeded threshold on {{ trigger.payload.host }}",
|
"summary": "{{ event.payload.metric_name }} exceeded threshold on {{ event.payload.host }}",
|
||||||
"severity": "critical",
|
"severity": "critical",
|
||||||
"source": "{{ trigger.payload.host }}",
|
"source": "{{ event.payload.host }}",
|
||||||
"custom_details": {
|
"custom_details": {
|
||||||
"metric": "{{ trigger.payload.metric_name }}",
|
"metric": "{{ event.payload.metric_name }}",
|
||||||
"current_value": "{{ trigger.payload.current_value }}",
|
"current_value": "{{ event.payload.current_value }}",
|
||||||
"threshold": "{{ trigger.payload.threshold }}",
|
"threshold": "{{ event.payload.threshold }}",
|
||||||
"duration": "{{ trigger.payload.duration_seconds }}s"
|
"duration": "{{ event.payload.duration_seconds }}s"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dedup_key": "{{ trigger.payload.host }}_{{ trigger.payload.metric_name }}"
|
"dedup_key": "{{ event.payload.host }}_{{ event.payload.metric_name }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -431,9 +431,12 @@ Apply transformations to values:
|
|||||||
1. **Rule Evaluation** - When an event matches a rule
|
1. **Rule Evaluation** - When an event matches a rule
|
||||||
2. **Template Extraction** - Identify `{{ }}` patterns in `action_params`
|
2. **Template Extraction** - Identify `{{ }}` patterns in `action_params`
|
||||||
3. **Context Building** - Assemble available data:
|
3. **Context Building** - Assemble available data:
|
||||||
- `trigger.payload` - Event payload data
|
- `event.id` - Event database ID
|
||||||
|
- `event.trigger` - Trigger ref that generated the event
|
||||||
|
- `event.created` - Event creation timestamp
|
||||||
|
- `event.payload` - Event payload data
|
||||||
- `pack.config` - Pack configuration
|
- `pack.config` - Pack configuration
|
||||||
- `system.*` - System-provided values
|
- `system.*` - System-provided values (timestamp, rule info)
|
||||||
4. **Value Resolution** - Extract values from context using dot notation paths
|
4. **Value Resolution** - Extract values from context using dot notation paths
|
||||||
5. **Type Conversion** - Preserve JSON types (string, number, boolean, object, array)
|
5. **Type Conversion** - Preserve JSON types (string, number, boolean, object, array)
|
||||||
6. **Parameter Assembly** - Build final parameter object
|
6. **Parameter Assembly** - Build final parameter object
|
||||||
@@ -444,7 +447,7 @@ Apply transformations to values:
|
|||||||
|
|
||||||
**Missing Values:**
|
**Missing Values:**
|
||||||
- If a referenced value doesn't exist and no default is provided, use `null`
|
- If a referenced value doesn't exist and no default is provided, use `null`
|
||||||
- Log warning: `"Template reference not found: trigger.payload.missing_field"`
|
- Log warning: `"Template variable not found: event.payload.missing_field"`
|
||||||
|
|
||||||
**Invalid Syntax:**
|
**Invalid Syntax:**
|
||||||
- If template syntax is invalid, log error and use the raw string
|
- If template syntax is invalid, log error and use the raw string
|
||||||
@@ -506,8 +509,8 @@ Pack configuration should be stored securely and can include:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"priority": "{{ trigger.payload.priority | default: 'medium' }}",
|
"priority": "{{ event.payload.priority | default: 'medium' }}",
|
||||||
"assignee": "{{ trigger.payload.assignee | default: 'unassigned' }}"
|
"assignee": "{{ event.payload.assignee | default: 'unassigned' }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -516,8 +519,8 @@ Pack configuration should be stored securely and can include:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"user_email": "{{ trigger.payload.user.email }}",
|
"user_email": "{{ event.payload.user.email }}",
|
||||||
"user_id": "{{ trigger.payload.user.id }}"
|
"user_id": "{{ event.payload.user.id }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -528,7 +531,7 @@ If a value never changes, keep it static:
|
|||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"service_name": "my-service", // Static - never changes
|
"service_name": "my-service", // Static - never changes
|
||||||
"error_code": "{{ trigger.payload.code }}" // Dynamic - from event
|
"error_code": "{{ event.payload.code }}" // Dynamic - from event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -581,10 +584,12 @@ Look for the resolved parameters in the execution's `config` field:
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"config": {
|
"config": {
|
||||||
"message": "Test message", // Resolved from trigger.payload.message
|
"message": "Test message", // Resolved from event.payload.message
|
||||||
"severity": "info", // Resolved from trigger.payload.severity
|
"severity": "info", // Resolved from event.payload.severity
|
||||||
"user_id": 123, // Resolved from trigger.payload.user.id
|
"user_id": 123, // Resolved from event.payload.user.id
|
||||||
"user_name": "Alice" // Resolved from trigger.payload.user.name
|
"user_name": "Alice" // Resolved from event.payload.user.name
|
||||||
|
"event_id": 456, // Resolved from event.id
|
||||||
|
"trigger": "core.test_event" // Resolved from event.trigger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -608,7 +613,7 @@ Look for the resolved parameters in the execution's `config` field:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"message": "Error: {{ trigger.payload.message }}"
|
"message": "Error: {{ event.payload.message }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -679,15 +684,15 @@ Look for the resolved parameters in the execution's `config` field:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "{% if trigger.payload.severity == 'critical' %}#incidents{% else %}#monitoring{% endif %}"
|
"channel": "{% if event.payload.severity == 'critical' %}#incidents{% else %}#monitoring{% endif %}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Advanced Filters
|
### 2. Advanced Filters
|
||||||
- Mathematical operations: `{{ trigger.payload.value | multiply: 100 }}`
|
- Mathematical operations: `{{ event.payload.value | multiply: 100 }}`
|
||||||
- String manipulation: `{{ trigger.payload.text | replace: 'old', 'new' }}`
|
- String manipulation: `{{ event.payload.text | replace: 'old', 'new' }}`
|
||||||
- Array operations: `{{ trigger.payload.items | join: ', ' }}`
|
- Array operations: `{{ event.payload.items | join: ', ' }}`
|
||||||
|
|
||||||
### 3. Custom Functions
|
### 3. Custom Functions
|
||||||
```json
|
```json
|
||||||
@@ -695,7 +700,7 @@ Look for the resolved parameters in the execution's `config` field:
|
|||||||
"action_params": {
|
"action_params": {
|
||||||
"timestamp": "{{ now() }}",
|
"timestamp": "{{ now() }}",
|
||||||
"uuid": "{{ uuid() }}",
|
"uuid": "{{ uuid() }}",
|
||||||
"hash": "{{ hash(trigger.payload.data) }}"
|
"hash": "{{ hash(event.payload.data) }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -704,7 +709,7 @@ Look for the resolved parameters in the execution's `config` field:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"user": "{{ trigger.payload.user | merge: pack.config.default_user }}"
|
"user": "{{ event.payload.user | merge: pack.config.default_user }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -734,7 +739,8 @@ Rule parameter mapping provides a powerful way to:
|
|||||||
|
|
||||||
**Key Concepts:**
|
**Key Concepts:**
|
||||||
- Static values for constants
|
- Static values for constants
|
||||||
- `{{ trigger.payload.* }}` for event data
|
- `{{ event.payload.* }}` for event payload data
|
||||||
|
- `{{ event.id }}`, `{{ event.trigger }}`, `{{ event.created }}` for event metadata
|
||||||
- `{{ pack.config.* }}` for pack configuration
|
- `{{ pack.config.* }}` for pack configuration
|
||||||
- `{{ system.* }}` for system-provided values
|
- `{{ system.* }}` for system-provided values
|
||||||
- Filters and defaults for robust templates
|
- Filters and defaults for robust templates
|
||||||
|
|||||||
@@ -181,12 +181,12 @@ Both `trigger_params` and `conditions` can filter events, but they serve differe
|
|||||||
},
|
},
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"and": [
|
"and": [
|
||||||
{"var": "trigger.payload.status_code", ">=": 500},
|
{"var": "event.payload.status_code", ">=": 500},
|
||||||
{"var": "trigger.payload.retry_count", ">": 3},
|
{"var": "event.payload.retry_count", ">": 3},
|
||||||
{
|
{
|
||||||
"or": [
|
"or": [
|
||||||
{"var": "trigger.payload.endpoint", "in": ["/auth", "/payment"]},
|
{"var": "event.payload.endpoint", "in": ["/auth", "/payment"]},
|
||||||
{"var": "trigger.payload.customer_impact", "==": true}
|
{"var": "event.payload.customer_impact", "==": true}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -448,7 +448,7 @@ This improves performance by filtering earlier in the evaluation pipeline.
|
|||||||
"action_ref": "slack.post_message",
|
"action_ref": "slack.post_message",
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"channel": "#pull-requests",
|
"channel": "#pull-requests",
|
||||||
"message": "New PR: {{ trigger.payload.title }} by {{ trigger.payload.user }}"
|
"message": "New PR: {{ event.payload.title }} by {{ event.payload.user }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ RULE1=$(curl -s -X POST "$API_URL/api/v1/rules" \
|
|||||||
"interval": 1
|
"interval": 1
|
||||||
},
|
},
|
||||||
"action_params": {
|
"action_params": {
|
||||||
"message": "Hello from 1-second timer! Time: {{trigger.payload.executed_at}}"
|
"message": "Hello from 1-second timer! Time: {{event.payload.executed_at}}"
|
||||||
}
|
}
|
||||||
}')
|
}')
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ RULE3=$(curl -s -X POST "$API_URL/api/v1/rules" \
|
|||||||
"action_params": {
|
"action_params": {
|
||||||
"url": "https://httpbin.org/post",
|
"url": "https://httpbin.org/post",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"body": "{\"message\": \"Test from Attune\", \"timestamp\": \"{{trigger.payload.executed_at}}\", \"rule\": \"test.httpbin_post\"}",
|
"body": "{\"message\": \"Test from Attune\", \"timestamp\": \"{{event.payload.executed_at}}\", \"rule\": \"test.httpbin_post\"}",
|
||||||
"headers": {
|
"headers": {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"User-Agent": "Attune-Test/1.0"
|
"User-Agent": "Attune-Test/1.0"
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def test_rule_criteria_basic_filtering(client: AttuneClient, test_pack):
|
|||||||
"trigger": trigger_ref,
|
"trigger": trigger_ref,
|
||||||
"action": action_info,
|
"action": action_info,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"criteria": "{{ trigger.payload.level == 'info' }}",
|
"criteria": "{{ event.payload.level == 'info' }}",
|
||||||
}
|
}
|
||||||
|
|
||||||
rule_info_response = client.create_rule(rule_info_data)
|
rule_info_response = client.create_rule(rule_info_data)
|
||||||
@@ -95,7 +95,7 @@ def test_rule_criteria_basic_filtering(client: AttuneClient, test_pack):
|
|||||||
"trigger": trigger_ref,
|
"trigger": trigger_ref,
|
||||||
"action": action_error,
|
"action": action_error,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"criteria": "{{ trigger.payload.level == 'error' }}",
|
"criteria": "{{ event.payload.level == 'error' }}",
|
||||||
}
|
}
|
||||||
|
|
||||||
rule_error_response = client.create_rule(rule_error_data)
|
rule_error_response = client.create_rule(rule_error_data)
|
||||||
@@ -262,7 +262,7 @@ def test_rule_criteria_numeric_comparison(client: AttuneClient, test_pack):
|
|||||||
"trigger": trigger_ref,
|
"trigger": trigger_ref,
|
||||||
"action": action_low,
|
"action": action_low,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"criteria": "{{ trigger.payload.priority <= 3 }}",
|
"criteria": "{{ event.payload.priority <= 3 }}",
|
||||||
}
|
}
|
||||||
rule_low = client.create_rule(rule_low_data)
|
rule_low = client.create_rule(rule_low_data)
|
||||||
print(f"✓ Low priority rule created (priority <= 3)")
|
print(f"✓ Low priority rule created (priority <= 3)")
|
||||||
@@ -273,7 +273,7 @@ def test_rule_criteria_numeric_comparison(client: AttuneClient, test_pack):
|
|||||||
"trigger": trigger_ref,
|
"trigger": trigger_ref,
|
||||||
"action": action_high,
|
"action": action_high,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"criteria": "{{ trigger.payload.priority >= 7 }}",
|
"criteria": "{{ event.payload.priority >= 7 }}",
|
||||||
}
|
}
|
||||||
rule_high = client.create_rule(rule_high_data)
|
rule_high = client.create_rule(rule_high_data)
|
||||||
print(f"✓ High priority rule created (priority >= 7)")
|
print(f"✓ High priority rule created (priority >= 7)")
|
||||||
@@ -356,8 +356,8 @@ def test_rule_criteria_complex_expressions(client: AttuneClient, test_pack):
|
|||||||
|
|
||||||
# Criteria: (level == 'error' AND priority > 5) OR environment == 'production'
|
# Criteria: (level == 'error' AND priority > 5) OR environment == 'production'
|
||||||
complex_criteria = (
|
complex_criteria = (
|
||||||
"{{ (trigger.payload.level == 'error' and trigger.payload.priority > 5) "
|
"{{ (event.payload.level == 'error' and event.payload.priority > 5) "
|
||||||
"or trigger.payload.environment == 'production' }}"
|
"or event.payload.environment == 'production' }}"
|
||||||
)
|
)
|
||||||
|
|
||||||
rule_data = {
|
rule_data = {
|
||||||
@@ -450,7 +450,7 @@ def test_rule_criteria_list_membership(client: AttuneClient, test_pack):
|
|||||||
print("\n[STEP 2] Creating rule with list membership criteria...")
|
print("\n[STEP 2] Creating rule with list membership criteria...")
|
||||||
|
|
||||||
# Criteria: status in ['critical', 'urgent', 'high']
|
# Criteria: status in ['critical', 'urgent', 'high']
|
||||||
list_criteria = "{{ trigger.payload.status in ['critical', 'urgent', 'high'] }}"
|
list_criteria = "{{ event.payload.status in ['critical', 'urgent', 'high'] }}"
|
||||||
|
|
||||||
rule_data = {
|
rule_data = {
|
||||||
"name": f"List Membership Rule {unique_ref()}",
|
"name": f"List Membership Rule {unique_ref()}",
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ def test_conditional_workflow_branching(client: AttuneClient, test_pack):
|
|||||||
"action": workflow["ref"],
|
"action": workflow["ref"],
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"condition": "{{ trigger.payload.condition }}",
|
"condition": "{{ event.payload.condition }}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rule_response = client.post("/rules", json=rule_payload)
|
rule_response = client.post("/rules", json=rule_payload)
|
||||||
@@ -665,7 +665,7 @@ print(json.dumps({'result': result, 'step': 2}))
|
|||||||
"action": workflow["ref"],
|
"action": workflow["ref"],
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"input_text": "{{ trigger.payload.text }}",
|
"input_text": "{{ event.payload.text }}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rule_response = client.post("/rules", json=rule_payload)
|
rule_response = client.post("/rules", json=rule_payload)
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ def test_webhook_triggers_workflow_triggers_webhook(client: AttuneClient, test_p
|
|||||||
"action": final_action["ref"],
|
"action": final_action["ref"],
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"message": "{{ trigger.payload.message }}",
|
"message": "{{ event.payload.message }}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rule_b_response = client.post("/rules", json=rule_b_payload)
|
rule_b_response = client.post("/rules", json=rule_b_payload)
|
||||||
@@ -522,7 +522,7 @@ print(json.dumps({'transformed_value': transformed, 'original': value}))
|
|||||||
"action": transform_action["ref"],
|
"action": transform_action["ref"],
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"value": "{{ trigger.payload.input_value }}",
|
"value": "{{ event.payload.input_value }}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rule_a_response = client.post("/rules", json=rule_a_payload)
|
rule_a_response = client.post("/rules", json=rule_a_payload)
|
||||||
@@ -539,7 +539,7 @@ print(json.dumps({'transformed_value': transformed, 'original': value}))
|
|||||||
"action": final_action["ref"],
|
"action": final_action["ref"],
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"message": "Received: {{ trigger.payload.transformed_value }}",
|
"message": "Received: {{ event.payload.transformed_value }}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rule_b_response = client.post("/rules", json=rule_b_payload)
|
rule_b_response = client.post("/rules", json=rule_b_payload)
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ def test_rule_criteria_evaluation_notification(client: AttuneClient, test_pack):
|
|||||||
"trigger": trigger["ref"],
|
"trigger": trigger["ref"],
|
||||||
"action": action["ref"],
|
"action": action["ref"],
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"criteria": "{{ trigger.payload.environment == 'production' }}",
|
"criteria": "{{ event.payload.environment == 'production' }}",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"message": "Production deployment approved",
|
"message": "Production deployment approved",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -212,8 +212,8 @@ print(json.dumps(result))
|
|||||||
"action": action["ref"],
|
"action": action["ref"],
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"name": "{{ trigger.payload.name }}",
|
"name": "{{ event.payload.name }}",
|
||||||
"count": "{{ trigger.payload.count }}",
|
"count": "{{ event.payload.count }}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rule_response = client.post("/rules", json=rule_payload)
|
rule_response = client.post("/rules", json=rule_payload)
|
||||||
|
|||||||
@@ -442,9 +442,9 @@ class TestBasicAutomation:
|
|||||||
"action_ref": action_ref,
|
"action_ref": action_ref,
|
||||||
"trigger_ref": trigger_ref,
|
"trigger_ref": trigger_ref,
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"and": [{"var": "trigger.payload.body.message", "!=": None}]
|
"and": [{"var": "event.payload.body.message", "!=": None}]
|
||||||
},
|
},
|
||||||
"action_params": {"message": "{{ trigger.payload.body.message }}"},
|
"action_params": {"message": "{{ event.payload.body.message }}"},
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
100
work-summary/2026-02-05-template-resolver-refactor.md
Normal file
100
work-summary/2026-02-05-template-resolver-refactor.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Work Summary: Template Resolver Refactor
|
||||||
|
|
||||||
|
**Date:** 2026-02-05
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Moved the template resolver from the sensor crate to the common crate, renamed the template namespace from `trigger.payload` to `event.payload`, added event metadata fields, and wired the resolver into the executor's event processor for real template resolution during enforcement creation.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
1. **Wrong location:** The template resolver lived in `crates/sensor/` but template resolution happens in the executor when rules create enforcements. The sensor crate was the wrong home.
|
||||||
|
2. **Misleading naming:** Templates used `trigger.payload.*` to reference data that actually comes from the *event* payload. The trigger defines the schema, but the runtime data is on the event.
|
||||||
|
3. **Stub in executor:** The executor's `event_processor.rs` had a `resolve_action_params()` stub that just passed action_params through unresolved. Template resolution was never actually happening.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Template Resolver Moved to Common Crate
|
||||||
|
|
||||||
|
- **New file:** `crates/common/src/template_resolver.rs`
|
||||||
|
- **Registered in:** `crates/common/src/lib.rs` with re-exports of `TemplateContext` and `resolve_templates`
|
||||||
|
- **Sensor crate:** `crates/sensor/src/lib.rs` now re-exports from common (`pub use attune_common::template_resolver::*`) for backward compatibility
|
||||||
|
- **Deleted:** `crates/sensor/src/template_resolver.rs` (old location)
|
||||||
|
|
||||||
|
### Renamed `trigger.payload` → `event.payload`
|
||||||
|
|
||||||
|
The template namespace was changed from `trigger` to `event` across the entire codebase:
|
||||||
|
|
||||||
|
- **Template syntax:** `{{ trigger.payload.field }}` → `{{ event.payload.field }}`
|
||||||
|
- **Struct field:** `trigger_payload: JsonValue` → `event: JsonValue` (restructured as a JSON object with `payload`, `id`, `trigger`, `created` sub-keys)
|
||||||
|
- **Context routing:** `get_value()` now routes `event.*` paths with a skip count of 1 (instead of `trigger` with skip 2)
|
||||||
|
|
||||||
|
### New Event Metadata in Templates
|
||||||
|
|
||||||
|
The `event.*` namespace now provides access to event metadata alongside the payload:
|
||||||
|
|
||||||
|
| Template | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `{{ event.payload.* }}` | Event payload fields (same data as before, just renamed) |
|
||||||
|
| `{{ event.id }}` | Event database ID (i64) |
|
||||||
|
| `{{ event.trigger }}` | Trigger ref that generated the event |
|
||||||
|
| `{{ event.created }}` | Event creation timestamp (RFC 3339) |
|
||||||
|
|
||||||
|
Builder methods on `TemplateContext`:
|
||||||
|
- `.with_event_id(id: i64)`
|
||||||
|
- `.with_event_trigger(trigger_ref: &str)`
|
||||||
|
- `.with_event_created(created: &str)`
|
||||||
|
|
||||||
|
### Executor Integration
|
||||||
|
|
||||||
|
`crates/executor/src/event_processor.rs` — `resolve_action_params()` was rewritten from a pass-through stub to a real implementation:
|
||||||
|
|
||||||
|
- Builds a `TemplateContext` from the `Event` and `Rule` models
|
||||||
|
- Populates `event.id`, `event.trigger`, `event.created`, and `event.payload` from the Event
|
||||||
|
- Populates `system.timestamp`, `system.rule.id`, `system.rule.ref` from the Rule and current time
|
||||||
|
- Pack config is currently passed as `{}` (TODO: load from database)
|
||||||
|
- Calls `resolve_templates()` on the rule's `action_params`
|
||||||
|
|
||||||
|
### Documentation Updates
|
||||||
|
|
||||||
|
All documentation files updated from `trigger.payload` to `event.payload`:
|
||||||
|
|
||||||
|
- `docs/api/api-rules.md`
|
||||||
|
- `docs/cli/cli.md`
|
||||||
|
- `docs/examples/rule-parameter-examples.md`
|
||||||
|
- `docs/guides/quickstart-example.md`
|
||||||
|
- `docs/workflows/dynamic-parameter-forms.md`
|
||||||
|
- `docs/workflows/parameter-mapping-status.md` (also overhauled to reflect completed implementation)
|
||||||
|
- `docs/workflows/rule-parameter-mapping.md` (section headers and prose updated too)
|
||||||
|
- `docs/workflows/rule-trigger-params.md`
|
||||||
|
- `crates/cli/README.md`
|
||||||
|
|
||||||
|
### Test and Script Updates
|
||||||
|
|
||||||
|
- `scripts/setup-test-rules.sh` — updated template references
|
||||||
|
- `tests/e2e/tier3/test_t3_05_rule_criteria.py`
|
||||||
|
- `tests/e2e/tier3/test_t3_07_complex_workflows.py`
|
||||||
|
- `tests/e2e/tier3/test_t3_08_chained_webhooks.py`
|
||||||
|
- `tests/e2e/tier3/test_t3_16_rule_notifications.py`
|
||||||
|
- `tests/e2e/tier3/test_t3_17_container_runner.py`
|
||||||
|
- `tests/test_e2e_basic.py`
|
||||||
|
|
||||||
|
### Docker Fix (Separate Issue)
|
||||||
|
|
||||||
|
Fixed `ARG NODE_VERSION` scoping in Docker multi-stage builds:
|
||||||
|
- `docker/Dockerfile.sensor.optimized` — added `ARG NODE_VERSION=20` inside `sensor-full` stage
|
||||||
|
- `docker/Dockerfile.worker.optimized` — added `ARG NODE_VERSION=20` inside `worker-node` and `worker-full` stages
|
||||||
|
|
||||||
|
Global ARGs are only available in `FROM` instructions; they must be re-declared inside stages to use in `RUN` commands.
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
- **20 unit tests** in `attune_common::template_resolver` — all pass
|
||||||
|
- **78 executor lib tests** — all pass
|
||||||
|
- **Sensor tests** — all pass (re-export works correctly)
|
||||||
|
- **Zero compiler warnings** across workspace
|
||||||
|
|
||||||
|
## Remaining Work
|
||||||
|
|
||||||
|
- **Pack config loading:** The executor currently passes `{}` for pack config context. `{{ pack.config.* }}` templates will resolve to null until pack config is loaded from the database.
|
||||||
|
- **Work summaries left as-is:** Historical work summaries in `work-summary/` still reference the old `trigger.payload` syntax. These are historical records and were intentionally not updated.
|
||||||
Reference in New Issue
Block a user