tftsr-devops_investigation/src-tauri/src/docs/rca.rs
2026-03-15 12:43:46 -05:00

226 lines
8.2 KiB
Rust

use crate::db::models::IssueDetail;
pub fn generate_rca_markdown(detail: &IssueDetail) -> String {
let issue = &detail.issue;
let mut md = String::new();
md.push_str(&format!("# Root Cause Analysis: {}\n\n", issue.title));
md.push_str("## Issue Summary\n\n");
md.push_str("| Field | Value |\n");
md.push_str("|-------|-------|\n");
md.push_str(&format!("| **Issue ID** | {} |\n", issue.id));
md.push_str(&format!("| **Category** | {} |\n", issue.category));
md.push_str(&format!("| **Status** | {} |\n", issue.status));
md.push_str(&format!("| **Severity** | {} |\n", issue.severity));
md.push_str(&format!("| **Source** | {} |\n", issue.source));
md.push_str(&format!(
"| **Assigned To** | {} |\n",
if issue.assigned_to.is_empty() {
"Unassigned"
} else {
&issue.assigned_to
}
));
md.push_str(&format!("| **Created** | {} |\n", issue.created_at));
md.push_str(&format!("| **Last Updated** | {} |\n", issue.updated_at));
if let Some(ref resolved) = issue.resolved_at {
md.push_str(&format!("| **Resolved** | {} |\n", resolved));
}
md.push_str("\n");
if !issue.description.is_empty() {
md.push_str("## Description\n\n");
md.push_str(&issue.description);
md.push_str("\n\n");
}
// 5 Whys Analysis
md.push_str("## 5 Whys Analysis\n\n");
if detail.resolution_steps.is_empty() {
md.push_str("_No 5-whys analysis has been performed yet._\n\n");
} else {
for step in &detail.resolution_steps {
md.push_str(&format!(
"### Why #{}: {}\n\n",
step.step_order, step.why_question
));
if !step.answer.is_empty() {
md.push_str(&format!("**Answer:** {}\n\n", step.answer));
} else {
md.push_str("_Awaiting answer._\n\n");
}
if !step.evidence.is_empty() {
md.push_str(&format!("**Evidence:** {}\n\n", step.evidence));
}
}
}
// Root Cause
md.push_str("## Root Cause\n\n");
if let Some(last_step) = detail.resolution_steps.last() {
if !last_step.answer.is_empty() {
md.push_str(&format!(
"Based on the 5-whys analysis, the root cause is:\n\n> {}\n\n",
last_step.answer
));
} else {
md.push_str(
"_The 5-whys analysis is incomplete. Complete it to identify the root cause._\n\n",
);
}
} else {
md.push_str("_Perform the 5-whys analysis to identify the root cause._\n\n");
}
// Log Files
md.push_str("## Log Files Analyzed\n\n");
if detail.log_files.is_empty() {
md.push_str("_No log files attached._\n\n");
} else {
md.push_str("| File | Size | Redacted | Hash |\n");
md.push_str("|------|------|----------|------|\n");
for lf in &detail.log_files {
md.push_str(&format!(
"| {} | {} bytes | {} | {}... |\n",
lf.file_name,
lf.file_size,
if lf.redacted { "Yes" } else { "No" },
&lf.content_hash[..8.min(lf.content_hash.len())],
));
}
md.push_str("\n");
}
// Corrective Actions
md.push_str("## Corrective Actions\n\n");
md.push_str("### Immediate Actions\n\n");
md.push_str("- [ ] _Document immediate mitigations taken_\n\n");
md.push_str("### Long-Term Actions\n\n");
md.push_str("- [ ] _Document preventive measures_\n");
md.push_str("- [ ] _Document monitoring improvements_\n\n");
// Lessons Learned
md.push_str("## Lessons Learned\n\n");
md.push_str("- _What went well during the response?_\n");
md.push_str("- _What could be improved?_\n");
md.push_str("- _What processes need updating?_\n\n");
md.push_str("---\n\n");
md.push_str(&format!(
"_Generated by TFTSR IT Triage on {}_\n",
chrono::Utc::now().format("%Y-%m-%d %H:%M UTC")
));
md
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::models::{Issue, IssueDetail, LogFile, ResolutionStep};
fn make_test_detail() -> IssueDetail {
IssueDetail {
issue: Issue {
id: "test-123".to_string(),
title: "Database connection timeout".to_string(),
description: "Users report 500 errors on login.".to_string(),
severity: "high".to_string(),
status: "investigating".to_string(),
category: "database".to_string(),
source: "manual".to_string(),
created_at: "2025-01-15 10:00:00".to_string(),
updated_at: "2025-01-15 12:00:00".to_string(),
resolved_at: None,
assigned_to: "oncall-eng".to_string(),
tags: "[]".to_string(),
},
log_files: vec![LogFile {
id: "lf-1".to_string(),
issue_id: "test-123".to_string(),
file_name: "app.log".to_string(),
file_path: "/tmp/app.log".to_string(),
file_size: 2048,
mime_type: "text/plain".to_string(),
content_hash: "abc123def456".to_string(),
uploaded_at: "2025-01-15 10:30:00".to_string(),
redacted: false,
}],
resolution_steps: vec![
ResolutionStep {
id: "rs-1".to_string(),
issue_id: "test-123".to_string(),
step_order: 1,
why_question: "Why are users getting 500 errors?".to_string(),
answer: "The database connection pool is exhausted.".to_string(),
evidence: "Connection pool metrics show 100/100 used.".to_string(),
created_at: "2025-01-15 11:00:00".to_string(),
},
ResolutionStep {
id: "rs-2".to_string(),
issue_id: "test-123".to_string(),
step_order: 2,
why_question: "Why is the connection pool exhausted?".to_string(),
answer: "Queries are not being released after completion.".to_string(),
evidence: "".to_string(),
created_at: "2025-01-15 11:15:00".to_string(),
},
],
conversations: vec![],
}
}
#[test]
fn test_rca_contains_title() {
let md = generate_rca_markdown(&make_test_detail());
assert!(md.contains("# Root Cause Analysis: Database connection timeout"));
}
#[test]
fn test_rca_contains_issue_summary_table() {
let md = generate_rca_markdown(&make_test_detail());
assert!(md.contains("| **Issue ID** | test-123 |"));
assert!(md.contains("| **Severity** | high |"));
assert!(md.contains("| **Assigned To** | oncall-eng |"));
}
#[test]
fn test_rca_contains_five_whys() {
let md = generate_rca_markdown(&make_test_detail());
assert!(md.contains("### Why #1: Why are users getting 500 errors?"));
assert!(md.contains("**Answer:** The database connection pool is exhausted."));
assert!(md.contains("**Evidence:** Connection pool metrics"));
}
#[test]
fn test_rca_contains_root_cause() {
let md = generate_rca_markdown(&make_test_detail());
assert!(md.contains("Queries are not being released after completion."));
}
#[test]
fn test_rca_contains_log_files() {
let md = generate_rca_markdown(&make_test_detail());
assert!(md.contains("app.log"));
assert!(md.contains("2048 bytes"));
}
#[test]
fn test_rca_empty_steps_shows_placeholder() {
let mut detail = make_test_detail();
detail.resolution_steps.clear();
let md = generate_rca_markdown(&detail);
assert!(md.contains("No 5-whys analysis has been performed yet."));
}
#[test]
fn test_rca_unassigned_shows_unassigned() {
let mut detail = make_test_detail();
detail.issue.assigned_to = String::new();
let md = generate_rca_markdown(&detail);
assert!(md.contains("Unassigned"));
}
}