7.0 KiB
Event Generation Fix and Timer Sensor Cleanup
Date: 2026-02-04
Status: Complete ✅
Problem
Events were not being generated and inserted into the database, despite rules being configured. The timer sensor was outputting JSON to stdout but events were never created in the database.
Root Cause Analysis
The system had two timer sensor implementations:
-
attune-timer-sensor(669KB) - Old subprocess-based sensor- Located in
crates/timer-sensor-subprocess/ - Outputs JSON events to stdout
- Expects sensor manager to parse stdout and create events
- Does NOT follow the documented sensor protocol
- Located in
-
attune-core-timer-sensor(5.8MB) - Correct API-based sensor- Located in
crates/sensor-timer/ - Makes HTTP POST requests to
/eventsAPI endpoint - Follows the documented sensor interface specification
- Handles rule lifecycle, token refresh, and proper event creation
- Located in
The database was configured to use the wrong sensor (attune-timer-sensor), causing events to be logged to stdout but never persisted to the database.
Investigation Steps
-
Initial symptom: Events visible in logs but not in database
"fired_at":"2026-02-04T01:21:28.028878792+00:00" -
Discovered sensor manager wasn't processing stdout:
- Sensor manager was just logging stdout, not parsing JSON
- No event creation or rule matching logic in sensor manager
-
Found documentation mismatch:
docs/sensors/sensor-interface.mdspecifies sensors should POST to/eventsAPI- Old subprocess sensor was outputting to stdout instead
-
Identified dual implementations:
- Both sensors were being built in Docker
- Database pointed to wrong one
Solution
Part 1: Switch to Correct Sensor Binary
Database Update:
UPDATE sensor
SET entrypoint = 'attune-core-timer-sensor'
WHERE ref = 'core.interval_timer_sensor';
Fixed Trigger Parameters: The proper sensor expects a tagged enum format:
{
"type": "interval",
"interval": 1,
"unit": "seconds"
}
Updated rules to include the missing "type" field:
UPDATE rule
SET trigger_params = '{"type": "interval", "interval": 1, "unit": "seconds"}'
WHERE trigger = 3;
Part 2: Remove Old Sensor Implementation
To prevent future confusion, completely removed the old subprocess-based sensor:
Files Deleted:
crates/timer-sensor-subprocess/- entire crate directory
Files Modified:
Cargo.toml- removedtimer-sensor-subprocessfrom workspace membersdocker/Dockerfile- removed build steps forattune-timer-sensorbinarypacks/core/sensors/interval_timer_sensor.yaml- updatedentry_pointtoattune-core-timer-sensor
Verification
After the fix, the complete event-driven automation pipeline is working:
Timer fires → Event created → Rule evaluated → Enforcement created → Execution runs → Action completes
Database verification:
-- Events being created every second
SELECT id, trigger_ref, created FROM event ORDER BY id DESC LIMIT 5;
-- Results: id=965-974, trigger_ref=core.intervaltimer
-- Enforcements created for matching rules
SELECT id, rule_ref, event, status FROM enforcement ORDER BY id DESC LIMIT 5;
-- Results: Both rules (default.echo_every_second, core.echo_every_second) creating enforcements
-- Executions running and completing
SELECT id, action_ref, status, result->'stdout' FROM execution ORDER BY id DESC LIMIT 5;
-- Results: status=completed, stdout="Hello, World!\n"
Technical Details
Sensor Manager Enhancement
Added fetch_trigger_instances() method to query enabled rules and pass their configurations to sensor subprocesses:
File: crates/sensor/src/sensor_manager.rs
async fn fetch_trigger_instances(&self, trigger_id: Id) -> Result<Vec<serde_json::Value>> {
let rows = sqlx::query(
r#"
SELECT *
FROM rule
WHERE trigger = $1
AND enabled = TRUE
"#,
)
.bind(trigger_id)
.fetch_all(&self.inner.db)
.await?;
let trigger_instances: Vec<serde_json::Value> = rows
.into_iter()
.map(|row| {
let id: i64 = row.try_get("id").unwrap_or(0);
let ref_str: String = row.try_get("ref").unwrap_or_default();
let trigger_params: serde_json::Value = row
.try_get("trigger_params")
.unwrap_or(serde_json::json!({}));
serde_json::json!({
"id": id,
"ref": ref_str,
"config": trigger_params
})
})
.collect();
Ok(trigger_instances)
}
The sensor subprocess receives this via ATTUNE_SENSOR_TRIGGERS environment variable.
Sensor Protocol Compliance
The correct sensor (attune-core-timer-sensor) follows the documented protocol:
-
Initialization:
- Reads configuration from environment variables
- Validates API connectivity
- Provisions authentication token
-
Rule Discovery:
- Fetches active rules for its trigger type from API
- Subscribes to RabbitMQ for rule lifecycle events (created, enabled, disabled, deleted)
-
Event Generation:
- Monitors timers based on rule configurations
- POSTs to
/eventsAPI endpoint when timers fire - API handles event creation, rule matching, and enforcement creation
-
Token Management:
- Automatically refreshes tokens at 80% of TTL
- Handles token expiration gracefully
Remaining Artifacts
The old binary attune-timer-sensor may still exist in:
- Running Docker containers (until rebuilt)
- Local
target/build directories (until cleaned) - Existing Docker images (until rebuilt)
These will be naturally cleaned up through normal build/deploy cycles.
Lessons Learned
- Documentation is critical: The sensor interface spec was correct, but an outdated implementation was still in the codebase
- Name similarity caused confusion:
attune-timer-sensorvsattune-core-timer-sensorare easy to confuse - Remove deprecated code promptly: Having two implementations led to using the wrong one
- Database-driven configuration: The sensor
entrypointin the database must match the correct binary name
Impact
- ✅ Events are now being created and persisted to database
- ✅ Rules are being evaluated and enforcements created
- ✅ Actions are executing successfully
- ✅ Complete end-to-end automation pipeline functional
- ✅ Codebase cleaned up (removed ~1,200 lines of obsolete code)
- ✅ Documentation and implementation now aligned
Related Files
Modified:
attune/crates/sensor/src/sensor_manager.rs- Added trigger instance fetchingattune/Cargo.toml- Removed old crate from workspaceattune/docker/Dockerfile- Removed old binary buildattune/packs/core/sensors/interval_timer_sensor.yaml- Updated entry_point
Deleted:
attune/crates/timer-sensor-subprocess/(entire directory)
Database:
- Updated
sensor.entrypointforcore.interval_timer_sensor - Updated
rule.trigger_paramsformat to include"type"field