Skip to content

Commit b740432

Browse files
authored
Merge pull request #61 from temiport25/main
feat: implement alert configuration and event logging
2 parents 373236e + 413f89d commit b740432

14 files changed

Lines changed: 2280 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
# Cancel in-progress runs for the same branch on new pushes.
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
env:
14+
CARGO_TERM_COLOR: always
15+
RUST_BACKTRACE: 1
16+
17+
jobs:
18+
# ---------------------------------------------------------------------------
19+
# Job 1: check — fast-fail on formatting errors and clippy warnings.
20+
# Runs first; if it fails the more expensive test/build jobs are skipped.
21+
# ---------------------------------------------------------------------------
22+
check:
23+
name: Format & Lint
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Install Rust (stable + rustfmt + clippy)
29+
uses: dtolnay/rust-toolchain@stable
30+
with:
31+
components: rustfmt, clippy
32+
33+
# Lightweight cache just for the registry index — no target/ needed here.
34+
- name: Cache Cargo registry
35+
uses: actions/cache@v4
36+
with:
37+
path: |
38+
~/.cargo/registry
39+
~/.cargo/git
40+
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
41+
restore-keys: |
42+
${{ runner.os }}-cargo-registry-
43+
44+
- name: Check formatting
45+
run: cargo fmt --all -- --check
46+
47+
- name: Run Clippy
48+
run: cargo clippy --all-targets --all-features -- -D warnings
49+
env:
50+
STELLAR_NETWORK: testnet
51+
POLL_INTERVAL_SECONDS: "60"
52+
DATABASE_URL: "sqlite::memory:"
53+
54+
# ---------------------------------------------------------------------------
55+
# Job 2: test — run the full test suite including integration tests.
56+
# ---------------------------------------------------------------------------
57+
test:
58+
name: Test Suite
59+
runs-on: ubuntu-latest
60+
needs: check
61+
env:
62+
STELLAR_NETWORK: testnet
63+
POLL_INTERVAL_SECONDS: "60"
64+
DATABASE_URL: "sqlite::memory:"
65+
RUST_LOG: error
66+
steps:
67+
- uses: actions/checkout@v4
68+
69+
- name: Install Rust (stable)
70+
uses: dtolnay/rust-toolchain@stable
71+
72+
- name: Cache Cargo artifacts
73+
uses: actions/cache@v4
74+
with:
75+
path: |
76+
~/.cargo/registry
77+
~/.cargo/git
78+
target
79+
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
80+
restore-keys: |
81+
${{ runner.os }}-cargo-test-
82+
${{ runner.os }}-cargo-
83+
84+
- name: Install SQLite system library
85+
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev
86+
87+
- name: Run all tests
88+
run: cargo test --all
89+
90+
# ---------------------------------------------------------------------------
91+
# Job 3: build — verify the release binary compiles cleanly.
92+
# Runs in parallel with test; both depend on check passing first.
93+
# ---------------------------------------------------------------------------
94+
build:
95+
name: Release Build
96+
runs-on: ubuntu-latest
97+
needs: check
98+
steps:
99+
- uses: actions/checkout@v4
100+
101+
- name: Install Rust (stable)
102+
uses: dtolnay/rust-toolchain@stable
103+
104+
- name: Cache Cargo artifacts
105+
uses: actions/cache@v4
106+
with:
107+
path: |
108+
~/.cargo/registry
109+
~/.cargo/git
110+
target
111+
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
112+
restore-keys: |
113+
${{ runner.os }}-cargo-release-
114+
${{ runner.os }}-cargo-
115+
116+
- name: Install SQLite system library
117+
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev
118+
119+
- name: Build release binary
120+
run: cargo build --release
121+
env:
122+
STELLAR_NETWORK: testnet
123+
POLL_INTERVAL_SECONDS: "60"
124+
DATABASE_URL: "sqlite::memory:"

packages/core/Cargo.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ edition = "2021"
77
name = "stellar-fee-tracker"
88
path = "src/main.rs"
99

10+
[lib]
11+
name = "stellar_fee_tracker"
12+
path = "src/lib.rs"
13+
1014
[dependencies]
1115
# Web framework
1216
axum = { version = "0.7", features = ["json"] }
@@ -45,8 +49,13 @@ sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-native-tls", "mac
4549
# Async trait support (required for dyn-compatible async traits)
4650
async-trait = "0.1"
4751

52+
# Metrics
53+
prometheus = "0.13"
54+
4855
[dev-dependencies]
4956
tokio = { version = "1", features = ["full", "test-util"] }
5057
proptest = "1"
5158
tokio-test = "0.4"
52-
tower = "0.5"
59+
tower = { version = "0.5", features = ["util"] }
60+
http-body-util = "0.1"
61+
wiremock = "0.5"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Migration 002: Alert configurations
2+
-- Stores webhook targets for outbound fee-spike notifications.
3+
4+
CREATE TABLE IF NOT EXISTS alert_configs (
5+
id INTEGER PRIMARY KEY AUTOINCREMENT,
6+
webhook_url TEXT NOT NULL,
7+
threshold TEXT NOT NULL DEFAULT 'Major',
8+
enabled INTEGER NOT NULL DEFAULT 1,
9+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
10+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
11+
);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- Migration 003: Alert events log
2+
-- Records every triggered alert dispatch for auditing and debugging.
3+
4+
CREATE TABLE IF NOT EXISTS alert_events (
5+
id INTEGER PRIMARY KEY AUTOINCREMENT,
6+
config_id INTEGER REFERENCES alert_configs(id),
7+
severity TEXT NOT NULL,
8+
peak_fee INTEGER NOT NULL,
9+
baseline_fee REAL NOT NULL,
10+
spike_ratio REAL NOT NULL,
11+
webhook_url TEXT NOT NULL,
12+
delivered INTEGER NOT NULL DEFAULT 0, -- 1 = success, 0 = failed
13+
triggered_at TEXT NOT NULL
14+
);
15+
16+
CREATE INDEX IF NOT EXISTS idx_alert_events_triggered_at
17+
ON alert_events (triggered_at);

packages/core/src/alerts/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod webhook;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//! Webhook alert delivery.
2+
//!
3+
//! Dispatches HTTP POST notifications to registered webhook targets when a
4+
//! fee spike is detected. Every delivery attempt — successful or not — is
5+
//! persisted in the `alert_events` table via [`FeeRepository::log_alert_event`].
6+
//!
7+
//! This module is the integration point for Issue #31 (webhook delivery) and
8+
//! Issue #32 (alert history). The `dispatch` function is called by the
9+
//! scheduler / insights engine whenever a spike crosses an alert threshold.
10+
11+
use std::sync::Arc;
12+
13+
use chrono::Utc;
14+
15+
use crate::repository::{AlertEvent, FeeRepository};
16+
17+
/// Payload describing a triggered fee-spike alert.
18+
#[derive(Debug, Clone)]
19+
pub struct AlertPayload {
20+
/// The alert config row id that triggered this dispatch (if known).
21+
pub config_id: Option<i64>,
22+
/// Severity label, e.g. "Minor", "Major", "Critical".
23+
pub severity: String,
24+
/// Highest fee observed during the spike window (in stroops).
25+
pub peak_fee: i64,
26+
/// Rolling baseline fee used for comparison.
27+
pub baseline_fee: f64,
28+
/// `peak_fee / baseline_fee`.
29+
pub spike_ratio: f64,
30+
/// Destination webhook URL.
31+
pub webhook_url: String,
32+
}
33+
34+
/// Dispatch a webhook notification and log the outcome to the database.
35+
///
36+
/// The HTTP client (`reqwest`) is not yet wired up in this stub — the
37+
/// `delivered` flag defaults to `false` until Issue #31 lands and the full
38+
/// HTTP POST is implemented. The repository logging is fully functional.
39+
pub async fn dispatch(payload: AlertPayload, repository: Arc<FeeRepository>) {
40+
// TODO (Issue #31): perform the actual HTTP POST here and capture success.
41+
let delivered = false;
42+
43+
let event = AlertEvent {
44+
id: None,
45+
config_id: payload.config_id,
46+
severity: payload.severity.clone(),
47+
peak_fee: payload.peak_fee,
48+
baseline_fee: payload.baseline_fee,
49+
spike_ratio: payload.spike_ratio,
50+
webhook_url: payload.webhook_url.clone(),
51+
delivered,
52+
triggered_at: Utc::now().to_rfc3339(),
53+
};
54+
55+
if let Err(err) = repository.log_alert_event(&event).await {
56+
tracing::error!(
57+
"Failed to log alert event for webhook {}: {}",
58+
payload.webhook_url,
59+
err
60+
);
61+
}
62+
}
63+
64+
#[cfg(test)]
65+
mod tests {
66+
use super::*;
67+
use crate::db::create_pool;
68+
69+
#[tokio::test]
70+
async fn dispatch_logs_event_to_database() {
71+
let pool = create_pool("sqlite::memory:").await.unwrap();
72+
let repo = Arc::new(FeeRepository::new(pool));
73+
74+
let payload = AlertPayload {
75+
config_id: None,
76+
severity: "Major".to_string(),
77+
peak_fee: 8000,
78+
baseline_fee: 130.5,
79+
spike_ratio: 61.3,
80+
webhook_url: "https://hooks.example.com/test".to_string(),
81+
};
82+
83+
dispatch(payload, repo.clone()).await;
84+
85+
let events = repo.query_alert_history(10, None, None).await.unwrap();
86+
assert_eq!(events.len(), 1);
87+
assert_eq!(events[0].severity, "Major");
88+
assert_eq!(events[0].peak_fee, 8000);
89+
// delivered = false until Issue #31 implements the HTTP POST
90+
assert!(!events[0].delivered);
91+
}
92+
}

0 commit comments

Comments
 (0)