feat(graylog): add model layer and project module default
This commit is contained in:
parent
fc434fe560
commit
aecc4689e7
3 changed files with 741 additions and 0 deletions
699
src-tauri/src/models/graylog.rs
Normal file
699
src-tauri/src/models/graylog.rs
Normal file
|
|
@ -0,0 +1,699 @@
|
||||||
|
use rusqlite::{params, Connection, OptionalExtension, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GraylogCredentials {
|
||||||
|
pub id: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub base_url: String,
|
||||||
|
pub api_token_encrypted: String,
|
||||||
|
pub analyst_agent_id: String,
|
||||||
|
pub developer_agent_id: String,
|
||||||
|
pub stream_id: Option<String>,
|
||||||
|
pub query_filter: String,
|
||||||
|
pub polling_interval_minutes: i32,
|
||||||
|
pub lookback_minutes: i32,
|
||||||
|
pub score_threshold: i32,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GraylogCredentialsSafe {
|
||||||
|
pub id: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub base_url: String,
|
||||||
|
pub analyst_agent_id: String,
|
||||||
|
pub developer_agent_id: String,
|
||||||
|
pub stream_id: Option<String>,
|
||||||
|
pub query_filter: String,
|
||||||
|
pub polling_interval_minutes: i32,
|
||||||
|
pub lookback_minutes: i32,
|
||||||
|
pub score_threshold: i32,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GraylogSubject {
|
||||||
|
pub id: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub subject_key: String,
|
||||||
|
pub source: String,
|
||||||
|
pub normalized_message: String,
|
||||||
|
pub first_seen_at: String,
|
||||||
|
pub last_seen_at: String,
|
||||||
|
pub last_score: i32,
|
||||||
|
pub active_ticket_id: Option<String>,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GraylogDetection {
|
||||||
|
pub id: String,
|
||||||
|
pub subject_id: String,
|
||||||
|
pub window_start: String,
|
||||||
|
pub window_end: String,
|
||||||
|
pub critical_count: i32,
|
||||||
|
pub error_count: i32,
|
||||||
|
pub warning_count: i32,
|
||||||
|
pub total_count: i32,
|
||||||
|
pub last_seen_at: String,
|
||||||
|
pub score: i32,
|
||||||
|
pub triggered: bool,
|
||||||
|
pub triggered_ticket_id: Option<String>,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timestamp_now() -> String {
|
||||||
|
chrono::Utc::now().to_rfc3339()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn credentials_from_row(row: &rusqlite::Row) -> rusqlite::Result<GraylogCredentials> {
|
||||||
|
Ok(GraylogCredentials {
|
||||||
|
id: row.get(0)?,
|
||||||
|
project_id: row.get(1)?,
|
||||||
|
base_url: row.get(2)?,
|
||||||
|
api_token_encrypted: row.get(3)?,
|
||||||
|
analyst_agent_id: row.get(4)?,
|
||||||
|
developer_agent_id: row.get(5)?,
|
||||||
|
stream_id: row.get(6)?,
|
||||||
|
query_filter: row.get(7)?,
|
||||||
|
polling_interval_minutes: row.get(8)?,
|
||||||
|
lookback_minutes: row.get(9)?,
|
||||||
|
score_threshold: row.get(10)?,
|
||||||
|
created_at: row.get(11)?,
|
||||||
|
updated_at: row.get(12)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subject_from_row(row: &rusqlite::Row) -> rusqlite::Result<GraylogSubject> {
|
||||||
|
Ok(GraylogSubject {
|
||||||
|
id: row.get(0)?,
|
||||||
|
project_id: row.get(1)?,
|
||||||
|
subject_key: row.get(2)?,
|
||||||
|
source: row.get(3)?,
|
||||||
|
normalized_message: row.get(4)?,
|
||||||
|
first_seen_at: row.get(5)?,
|
||||||
|
last_seen_at: row.get(6)?,
|
||||||
|
last_score: row.get(7)?,
|
||||||
|
active_ticket_id: row.get(8)?,
|
||||||
|
status: row.get(9)?,
|
||||||
|
created_at: row.get(10)?,
|
||||||
|
updated_at: row.get(11)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detection_from_row(row: &rusqlite::Row) -> rusqlite::Result<GraylogDetection> {
|
||||||
|
let triggered_int: i32 = row.get(10)?;
|
||||||
|
|
||||||
|
Ok(GraylogDetection {
|
||||||
|
id: row.get(0)?,
|
||||||
|
subject_id: row.get(1)?,
|
||||||
|
window_start: row.get(2)?,
|
||||||
|
window_end: row.get(3)?,
|
||||||
|
critical_count: row.get(4)?,
|
||||||
|
error_count: row.get(5)?,
|
||||||
|
warning_count: row.get(6)?,
|
||||||
|
total_count: row.get(7)?,
|
||||||
|
last_seen_at: row.get(8)?,
|
||||||
|
score: row.get(9)?,
|
||||||
|
triggered: triggered_int != 0,
|
||||||
|
triggered_ticket_id: row.get(11)?,
|
||||||
|
created_at: row.get(12)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraylogCredentials {
|
||||||
|
pub fn upsert_for_project(
|
||||||
|
conn: &Connection,
|
||||||
|
project_id: &str,
|
||||||
|
base_url: &str,
|
||||||
|
api_token_encrypted: &str,
|
||||||
|
analyst_agent_id: &str,
|
||||||
|
developer_agent_id: &str,
|
||||||
|
stream_id: Option<&str>,
|
||||||
|
query_filter: &str,
|
||||||
|
polling_interval_minutes: i32,
|
||||||
|
lookback_minutes: i32,
|
||||||
|
score_threshold: i32,
|
||||||
|
) -> Result<GraylogCredentials> {
|
||||||
|
let now = timestamp_now();
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO graylog_credentials (
|
||||||
|
id,
|
||||||
|
project_id,
|
||||||
|
base_url,
|
||||||
|
api_token_encrypted,
|
||||||
|
analyst_agent_id,
|
||||||
|
developer_agent_id,
|
||||||
|
stream_id,
|
||||||
|
query_filter,
|
||||||
|
polling_interval_minutes,
|
||||||
|
lookback_minutes,
|
||||||
|
score_threshold,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
|
||||||
|
ON CONFLICT(project_id) DO UPDATE SET
|
||||||
|
base_url = excluded.base_url,
|
||||||
|
api_token_encrypted = excluded.api_token_encrypted,
|
||||||
|
analyst_agent_id = excluded.analyst_agent_id,
|
||||||
|
developer_agent_id = excluded.developer_agent_id,
|
||||||
|
stream_id = excluded.stream_id,
|
||||||
|
query_filter = excluded.query_filter,
|
||||||
|
polling_interval_minutes = excluded.polling_interval_minutes,
|
||||||
|
lookback_minutes = excluded.lookback_minutes,
|
||||||
|
score_threshold = excluded.score_threshold,
|
||||||
|
updated_at = excluded.updated_at",
|
||||||
|
params![
|
||||||
|
id,
|
||||||
|
project_id,
|
||||||
|
base_url,
|
||||||
|
api_token_encrypted,
|
||||||
|
analyst_agent_id,
|
||||||
|
developer_agent_id,
|
||||||
|
stream_id,
|
||||||
|
query_filter,
|
||||||
|
polling_interval_minutes,
|
||||||
|
lookback_minutes,
|
||||||
|
score_threshold,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Self::get_by_project(conn, project_id)?.ok_or(rusqlite::Error::QueryReturnedNoRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_project(
|
||||||
|
conn: &Connection,
|
||||||
|
project_id: &str,
|
||||||
|
) -> Result<Option<GraylogCredentials>> {
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT
|
||||||
|
id,
|
||||||
|
project_id,
|
||||||
|
base_url,
|
||||||
|
api_token_encrypted,
|
||||||
|
analyst_agent_id,
|
||||||
|
developer_agent_id,
|
||||||
|
stream_id,
|
||||||
|
query_filter,
|
||||||
|
polling_interval_minutes,
|
||||||
|
lookback_minutes,
|
||||||
|
score_threshold,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM graylog_credentials
|
||||||
|
WHERE project_id = ?1
|
||||||
|
LIMIT 1",
|
||||||
|
params![project_id],
|
||||||
|
credentials_from_row,
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_for_project(conn: &Connection, project_id: &str) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM graylog_credentials WHERE project_id = ?1",
|
||||||
|
params![project_id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_safe(&self) -> GraylogCredentialsSafe {
|
||||||
|
GraylogCredentialsSafe {
|
||||||
|
id: self.id.clone(),
|
||||||
|
project_id: self.project_id.clone(),
|
||||||
|
base_url: self.base_url.clone(),
|
||||||
|
analyst_agent_id: self.analyst_agent_id.clone(),
|
||||||
|
developer_agent_id: self.developer_agent_id.clone(),
|
||||||
|
stream_id: self.stream_id.clone(),
|
||||||
|
query_filter: self.query_filter.clone(),
|
||||||
|
polling_interval_minutes: self.polling_interval_minutes,
|
||||||
|
lookback_minutes: self.lookback_minutes,
|
||||||
|
score_threshold: self.score_threshold,
|
||||||
|
created_at: self.created_at.clone(),
|
||||||
|
updated_at: self.updated_at.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraylogSubject {
|
||||||
|
pub fn upsert_subject(
|
||||||
|
conn: &Connection,
|
||||||
|
project_id: &str,
|
||||||
|
subject_key: &str,
|
||||||
|
source: &str,
|
||||||
|
normalized_message: &str,
|
||||||
|
last_seen_at: &str,
|
||||||
|
last_score: i32,
|
||||||
|
) -> Result<GraylogSubject> {
|
||||||
|
let now = timestamp_now();
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO graylog_subjects (
|
||||||
|
id,
|
||||||
|
project_id,
|
||||||
|
subject_key,
|
||||||
|
source,
|
||||||
|
normalized_message,
|
||||||
|
first_seen_at,
|
||||||
|
last_seen_at,
|
||||||
|
last_score,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6, ?7, 'idle', ?8, ?8)
|
||||||
|
ON CONFLICT(project_id, subject_key) DO UPDATE SET
|
||||||
|
source = excluded.source,
|
||||||
|
normalized_message = excluded.normalized_message,
|
||||||
|
last_seen_at = excluded.last_seen_at,
|
||||||
|
last_score = excluded.last_score,
|
||||||
|
updated_at = excluded.updated_at",
|
||||||
|
params![
|
||||||
|
id,
|
||||||
|
project_id,
|
||||||
|
subject_key,
|
||||||
|
source,
|
||||||
|
normalized_message,
|
||||||
|
last_seen_at,
|
||||||
|
last_score,
|
||||||
|
now,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Self::get_by_project_and_key(conn, project_id, subject_key)?
|
||||||
|
.ok_or(rusqlite::Error::QueryReturnedNoRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_project_and_key(
|
||||||
|
conn: &Connection,
|
||||||
|
project_id: &str,
|
||||||
|
subject_key: &str,
|
||||||
|
) -> Result<Option<GraylogSubject>> {
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT
|
||||||
|
id,
|
||||||
|
project_id,
|
||||||
|
subject_key,
|
||||||
|
source,
|
||||||
|
normalized_message,
|
||||||
|
first_seen_at,
|
||||||
|
last_seen_at,
|
||||||
|
last_score,
|
||||||
|
active_ticket_id,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM graylog_subjects
|
||||||
|
WHERE project_id = ?1
|
||||||
|
AND subject_key = ?2
|
||||||
|
LIMIT 1",
|
||||||
|
params![project_id, subject_key],
|
||||||
|
subject_from_row,
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_active_ticket(
|
||||||
|
conn: &Connection,
|
||||||
|
subject_id: &str,
|
||||||
|
active_ticket_id: Option<&str>,
|
||||||
|
status: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let now = timestamp_now();
|
||||||
|
let affected = conn.execute(
|
||||||
|
"UPDATE graylog_subjects
|
||||||
|
SET active_ticket_id = ?1,
|
||||||
|
status = ?2,
|
||||||
|
updated_at = ?3
|
||||||
|
WHERE id = ?4",
|
||||||
|
params![active_ticket_id, status, now, subject_id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return Err(rusqlite::Error::QueryReturnedNoRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_by_project(conn: &Connection, project_id: &str) -> Result<Vec<GraylogSubject>> {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT
|
||||||
|
id,
|
||||||
|
project_id,
|
||||||
|
subject_key,
|
||||||
|
source,
|
||||||
|
normalized_message,
|
||||||
|
first_seen_at,
|
||||||
|
last_seen_at,
|
||||||
|
last_score,
|
||||||
|
active_ticket_id,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM graylog_subjects
|
||||||
|
WHERE project_id = ?1
|
||||||
|
ORDER BY last_seen_at DESC, created_at DESC, id DESC",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(params![project_id], subject_from_row)?;
|
||||||
|
rows.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraylogDetection {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn insert(
|
||||||
|
conn: &Connection,
|
||||||
|
subject_id: &str,
|
||||||
|
window_start: &str,
|
||||||
|
window_end: &str,
|
||||||
|
critical_count: i32,
|
||||||
|
error_count: i32,
|
||||||
|
warning_count: i32,
|
||||||
|
total_count: i32,
|
||||||
|
last_seen_at: &str,
|
||||||
|
score: i32,
|
||||||
|
triggered: bool,
|
||||||
|
triggered_ticket_id: Option<&str>,
|
||||||
|
) -> Result<GraylogDetection> {
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
let created_at = timestamp_now();
|
||||||
|
let triggered_int = if triggered { 1i32 } else { 0i32 };
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO graylog_detections (
|
||||||
|
id,
|
||||||
|
subject_id,
|
||||||
|
window_start,
|
||||||
|
window_end,
|
||||||
|
critical_count,
|
||||||
|
error_count,
|
||||||
|
warning_count,
|
||||||
|
total_count,
|
||||||
|
last_seen_at,
|
||||||
|
score,
|
||||||
|
triggered,
|
||||||
|
triggered_ticket_id,
|
||||||
|
created_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||||
|
params![
|
||||||
|
id,
|
||||||
|
subject_id,
|
||||||
|
window_start,
|
||||||
|
window_end,
|
||||||
|
critical_count,
|
||||||
|
error_count,
|
||||||
|
warning_count,
|
||||||
|
total_count,
|
||||||
|
last_seen_at,
|
||||||
|
score,
|
||||||
|
triggered_int,
|
||||||
|
triggered_ticket_id,
|
||||||
|
created_at,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT
|
||||||
|
id,
|
||||||
|
subject_id,
|
||||||
|
window_start,
|
||||||
|
window_end,
|
||||||
|
critical_count,
|
||||||
|
error_count,
|
||||||
|
warning_count,
|
||||||
|
total_count,
|
||||||
|
last_seen_at,
|
||||||
|
score,
|
||||||
|
triggered,
|
||||||
|
triggered_ticket_id,
|
||||||
|
created_at
|
||||||
|
FROM graylog_detections
|
||||||
|
WHERE id = ?1",
|
||||||
|
params![id],
|
||||||
|
detection_from_row,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_by_project(
|
||||||
|
conn: &Connection,
|
||||||
|
project_id: &str,
|
||||||
|
subject_id: Option<&str>,
|
||||||
|
) -> Result<Vec<GraylogDetection>> {
|
||||||
|
let mut stmt = if subject_id.is_some() {
|
||||||
|
conn.prepare(
|
||||||
|
"SELECT
|
||||||
|
gd.id,
|
||||||
|
gd.subject_id,
|
||||||
|
gd.window_start,
|
||||||
|
gd.window_end,
|
||||||
|
gd.critical_count,
|
||||||
|
gd.error_count,
|
||||||
|
gd.warning_count,
|
||||||
|
gd.total_count,
|
||||||
|
gd.last_seen_at,
|
||||||
|
gd.score,
|
||||||
|
gd.triggered,
|
||||||
|
gd.triggered_ticket_id,
|
||||||
|
gd.created_at
|
||||||
|
FROM graylog_detections gd
|
||||||
|
JOIN graylog_subjects gs ON gs.id = gd.subject_id
|
||||||
|
WHERE gs.project_id = ?1
|
||||||
|
AND gd.subject_id = ?2
|
||||||
|
ORDER BY gd.created_at DESC, gd.id DESC",
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
conn.prepare(
|
||||||
|
"SELECT
|
||||||
|
gd.id,
|
||||||
|
gd.subject_id,
|
||||||
|
gd.window_start,
|
||||||
|
gd.window_end,
|
||||||
|
gd.critical_count,
|
||||||
|
gd.error_count,
|
||||||
|
gd.warning_count,
|
||||||
|
gd.total_count,
|
||||||
|
gd.last_seen_at,
|
||||||
|
gd.score,
|
||||||
|
gd.triggered,
|
||||||
|
gd.triggered_ticket_id,
|
||||||
|
gd.created_at
|
||||||
|
FROM graylog_detections gd
|
||||||
|
JOIN graylog_subjects gs ON gs.id = gd.subject_id
|
||||||
|
WHERE gs.project_id = ?1
|
||||||
|
ORDER BY gd.created_at DESC, gd.id DESC",
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = if let Some(subject_id) = subject_id {
|
||||||
|
stmt.query_map(params![project_id, subject_id], detection_from_row)?
|
||||||
|
} else {
|
||||||
|
stmt.query_map(params![project_id], detection_from_row)?
|
||||||
|
};
|
||||||
|
|
||||||
|
rows.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::db;
|
||||||
|
use crate::models::agent::{Agent, AgentRole, AgentTool};
|
||||||
|
use crate::models::project::Project;
|
||||||
|
use crate::models::ticket::ProcessedTicket;
|
||||||
|
use rusqlite::Connection;
|
||||||
|
|
||||||
|
fn setup() -> (Connection, Project, Agent, Agent) {
|
||||||
|
let conn = db::init_in_memory().expect("db init should succeed");
|
||||||
|
let project = Project::insert(&conn, "Graylog", "/tmp/graylog", None, "main")
|
||||||
|
.expect("project insert should succeed");
|
||||||
|
let analyst = Agent::insert(&conn, "Analyst", AgentRole::Analyst, AgentTool::Codex, "")
|
||||||
|
.expect("analyst insert should succeed");
|
||||||
|
let developer = Agent::insert(
|
||||||
|
&conn,
|
||||||
|
"Developer",
|
||||||
|
AgentRole::Developer,
|
||||||
|
AgentTool::ClaudeCode,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.expect("developer insert should succeed");
|
||||||
|
|
||||||
|
(conn, project, analyst, developer)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_upsert_graylog_credentials_for_project() {
|
||||||
|
let (conn, project, analyst, developer) = setup();
|
||||||
|
|
||||||
|
let creds = GraylogCredentials::upsert_for_project(
|
||||||
|
&conn,
|
||||||
|
&project.id,
|
||||||
|
"https://graylog.local",
|
||||||
|
"enc-token",
|
||||||
|
&analyst.id,
|
||||||
|
&developer.id,
|
||||||
|
Some("stream-1"),
|
||||||
|
"level:(critical OR error)",
|
||||||
|
10,
|
||||||
|
30,
|
||||||
|
70,
|
||||||
|
)
|
||||||
|
.expect("upsert should succeed");
|
||||||
|
|
||||||
|
assert_eq!(creds.project_id, project.id);
|
||||||
|
assert_eq!(creds.base_url, "https://graylog.local");
|
||||||
|
assert_eq!(creds.api_token_encrypted, "enc-token");
|
||||||
|
assert_eq!(creds.stream_id.as_deref(), Some("stream-1"));
|
||||||
|
|
||||||
|
let stored = GraylogCredentials::get_by_project(&conn, &project.id)
|
||||||
|
.expect("get should succeed")
|
||||||
|
.expect("credentials should exist");
|
||||||
|
assert_eq!(stored.id, creds.id);
|
||||||
|
assert_eq!(stored.analyst_agent_id, analyst.id);
|
||||||
|
|
||||||
|
let safe = stored.to_safe();
|
||||||
|
assert_eq!(safe.project_id, project.id);
|
||||||
|
assert_eq!(safe.base_url, "https://graylog.local");
|
||||||
|
|
||||||
|
GraylogCredentials::delete_for_project(&conn, &project.id).expect("delete should succeed");
|
||||||
|
let deleted = GraylogCredentials::get_by_project(&conn, &project.id)
|
||||||
|
.expect("get after delete should succeed");
|
||||||
|
assert!(deleted.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_upsert_subject_and_get_by_project_and_key() {
|
||||||
|
let (conn, project, _, _) = setup();
|
||||||
|
|
||||||
|
let first_seen = "2026-04-17T08:00:00Z";
|
||||||
|
let second_seen = "2026-04-17T09:00:00Z";
|
||||||
|
|
||||||
|
let subject = GraylogSubject::upsert_subject(
|
||||||
|
&conn,
|
||||||
|
&project.id,
|
||||||
|
"api:timeout",
|
||||||
|
"api",
|
||||||
|
"timeout while calling payment gateway",
|
||||||
|
first_seen,
|
||||||
|
42,
|
||||||
|
)
|
||||||
|
.expect("initial subject upsert should succeed");
|
||||||
|
|
||||||
|
assert_eq!(subject.project_id, project.id);
|
||||||
|
assert_eq!(subject.subject_key, "api:timeout");
|
||||||
|
assert_eq!(subject.first_seen_at, first_seen);
|
||||||
|
assert_eq!(subject.last_seen_at, first_seen);
|
||||||
|
assert_eq!(subject.last_score, 42);
|
||||||
|
assert_eq!(subject.status, "idle");
|
||||||
|
assert!(subject.active_ticket_id.is_none());
|
||||||
|
|
||||||
|
let ticket = ProcessedTicket::insert_external(
|
||||||
|
&conn,
|
||||||
|
&project.id,
|
||||||
|
"graylog",
|
||||||
|
Some(&subject.id),
|
||||||
|
101,
|
||||||
|
"[Graylog] api - timeout",
|
||||||
|
"{\"subject_id\":\"api:timeout\"}",
|
||||||
|
)
|
||||||
|
.expect("ticket insert should succeed");
|
||||||
|
|
||||||
|
GraylogSubject::upsert_subject(
|
||||||
|
&conn,
|
||||||
|
&project.id,
|
||||||
|
"api:timeout",
|
||||||
|
"api",
|
||||||
|
"timeout while calling payment gateway",
|
||||||
|
second_seen,
|
||||||
|
73,
|
||||||
|
)
|
||||||
|
.expect("second subject upsert should succeed");
|
||||||
|
GraylogSubject::set_active_ticket(&conn, &subject.id, Some(&ticket.id), "queued")
|
||||||
|
.expect("set active ticket should succeed");
|
||||||
|
|
||||||
|
let stored = GraylogSubject::get_by_project_and_key(&conn, &project.id, "api:timeout")
|
||||||
|
.expect("get should succeed")
|
||||||
|
.expect("subject should exist");
|
||||||
|
assert_eq!(stored.id, subject.id);
|
||||||
|
assert_eq!(stored.first_seen_at, first_seen);
|
||||||
|
assert_eq!(stored.last_seen_at, second_seen);
|
||||||
|
assert_eq!(stored.last_score, 73);
|
||||||
|
assert_eq!(stored.active_ticket_id.as_deref(), Some(ticket.id.as_str()));
|
||||||
|
assert_eq!(stored.status, "queued");
|
||||||
|
|
||||||
|
let subjects =
|
||||||
|
GraylogSubject::list_by_project(&conn, &project.id).expect("list should succeed");
|
||||||
|
assert_eq!(subjects.len(), 1);
|
||||||
|
assert_eq!(subjects[0].id, subject.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_detection_and_list_by_project_orders_latest_first() {
|
||||||
|
let (conn, project, _, _) = setup();
|
||||||
|
let subject = GraylogSubject::upsert_subject(
|
||||||
|
&conn,
|
||||||
|
&project.id,
|
||||||
|
"worker:panic",
|
||||||
|
"worker",
|
||||||
|
"panic in background worker",
|
||||||
|
"2026-04-17T08:00:00Z",
|
||||||
|
55,
|
||||||
|
)
|
||||||
|
.expect("subject upsert should succeed");
|
||||||
|
|
||||||
|
let first = GraylogDetection::insert(
|
||||||
|
&conn,
|
||||||
|
&subject.id,
|
||||||
|
"2026-04-17T08:00:00Z",
|
||||||
|
"2026-04-17T08:10:00Z",
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
4,
|
||||||
|
"2026-04-17T08:09:00Z",
|
||||||
|
55,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("first detection insert should succeed");
|
||||||
|
let second = GraylogDetection::insert(
|
||||||
|
&conn,
|
||||||
|
&subject.id,
|
||||||
|
"2026-04-17T09:00:00Z",
|
||||||
|
"2026-04-17T09:10:00Z",
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
7,
|
||||||
|
"2026-04-17T09:09:00Z",
|
||||||
|
80,
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("second detection insert should succeed");
|
||||||
|
|
||||||
|
let detections = GraylogDetection::list_by_project(&conn, &project.id, None)
|
||||||
|
.expect("list should succeed");
|
||||||
|
assert_eq!(detections.len(), 2);
|
||||||
|
assert_eq!(detections[0].id, second.id);
|
||||||
|
assert_eq!(detections[1].id, first.id);
|
||||||
|
|
||||||
|
let subject_detections =
|
||||||
|
GraylogDetection::list_by_project(&conn, &project.id, Some(&subject.id))
|
||||||
|
.expect("subject list should succeed");
|
||||||
|
assert_eq!(subject_detections.len(), 2);
|
||||||
|
assert!(subject_detections[0].triggered);
|
||||||
|
assert_eq!(subject_detections[0].score, 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod agent;
|
pub mod agent;
|
||||||
pub mod agent_task;
|
pub mod agent_task;
|
||||||
pub mod credential;
|
pub mod credential;
|
||||||
|
pub mod graylog;
|
||||||
pub mod live_agent;
|
pub mod live_agent;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub const MODULE_TULEAP_AUTO_RESOLVE: &str = "tuleap_polling_auto_resolve";
|
pub const MODULE_TULEAP_AUTO_RESOLVE: &str = "tuleap_polling_auto_resolve";
|
||||||
|
pub const MODULE_GRAYLOG_AUTO_RESOLVE: &str = "graylog_polling_auto_resolve";
|
||||||
pub const MODULE_AI_LIVE_CHAT: &str = "ai_live_chat";
|
pub const MODULE_AI_LIVE_CHAT: &str = "ai_live_chat";
|
||||||
pub const MODULE_AGENT_TASK_RUNNER: &str = "agent_task_runner";
|
pub const MODULE_AGENT_TASK_RUNNER: &str = "agent_task_runner";
|
||||||
|
|
||||||
|
|
@ -70,6 +71,13 @@ impl ProjectModule {
|
||||||
"Polling Tuleap + auto-resolve",
|
"Polling Tuleap + auto-resolve",
|
||||||
"Surveille Tuleap et lance le pipeline analyste/developpeur.",
|
"Surveille Tuleap et lance le pipeline analyste/developpeur.",
|
||||||
)?;
|
)?;
|
||||||
|
insert_default(
|
||||||
|
conn,
|
||||||
|
project_id,
|
||||||
|
MODULE_GRAYLOG_AUTO_RESOLVE,
|
||||||
|
"Polling Graylog + auto-resolve",
|
||||||
|
"Surveille Graylog, score les sujets, et déclenche le pipeline analyste/developpeur.",
|
||||||
|
)?;
|
||||||
insert_default(
|
insert_default(
|
||||||
conn,
|
conn,
|
||||||
project_id,
|
project_id,
|
||||||
|
|
@ -140,3 +148,36 @@ impl ProjectModule {
|
||||||
Ok(enabled_int != 0)
|
Ok(enabled_int != 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::db;
|
||||||
|
use crate::models::project::Project;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ensure_defaults_for_project_includes_graylog_module() {
|
||||||
|
let conn = db::init_in_memory().expect("db init should succeed");
|
||||||
|
let project = Project::insert(&conn, "Modules", "/tmp/modules", None, "main")
|
||||||
|
.expect("project insert should succeed");
|
||||||
|
|
||||||
|
ProjectModule::ensure_defaults_for_project(&conn, &project.id)
|
||||||
|
.expect("ensure defaults should succeed");
|
||||||
|
|
||||||
|
let modules =
|
||||||
|
ProjectModule::list_by_project(&conn, &project.id).expect("list should succeed");
|
||||||
|
assert_eq!(modules.len(), 4);
|
||||||
|
|
||||||
|
let graylog = modules
|
||||||
|
.iter()
|
||||||
|
.find(|module| module.module_key == MODULE_GRAYLOG_AUTO_RESOLVE)
|
||||||
|
.expect("graylog module should be present");
|
||||||
|
|
||||||
|
assert_eq!(graylog.name, "Polling Graylog + auto-resolve");
|
||||||
|
assert_eq!(
|
||||||
|
graylog.description,
|
||||||
|
"Surveille Graylog, score les sujets, et déclenche le pipeline analyste/developpeur."
|
||||||
|
);
|
||||||
|
assert!(graylog.enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue