""" NV-Log Analyzer - SQLite 데이터베이스 모듈 분석 결과를 영구 저장하여 장애 케이스 이력을 축적합니다. """ import json import sqlite3 from contextlib import contextmanager from datetime import datetime from pathlib import Path DB_PATH = Path(__file__).parent / "data" / "nv_log_history.db" @contextmanager def get_conn(): """DB 커넥션 컨텍스트 매니저""" conn = sqlite3.connect(str(DB_PATH)) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() def init_db(): """DB 테이블 초기화 (앱 시작 시 1회 호출)""" with get_conn() as conn: conn.executescript(""" CREATE TABLE IF NOT EXISTS analysis ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')), filename TEXT NOT NULL, file_size_mb REAL, overall_status TEXT, total_issues INTEGER DEFAULT 0, critical_count INTEGER DEFAULT 0, warning_count INTEGER DEFAULT 0, info_count INTEGER DEFAULT 0, driver_version TEXT, cuda_version TEXT, gpu_count INTEGER DEFAULT 0, xid_count INTEGER DEFAULT 0, kernel_event_count INTEGER DEFAULT 0, rma_count INTEGER DEFAULT 0, ecc_info TEXT, memo TEXT DEFAULT '' ); CREATE TABLE IF NOT EXISTS analysis_gpu ( id INTEGER PRIMARY KEY AUTOINCREMENT, analysis_id INTEGER NOT NULL REFERENCES analysis(id) ON DELETE CASCADE, gpu_index INTEGER, name TEXT, serial_number TEXT, gpu_uuid TEXT, pci_address TEXT, product_brand TEXT, product_architecture TEXT, driver_version TEXT, cuda_version TEXT, vbios_version TEXT, inforom_version TEXT, temperature INTEGER, temperature_limit TEXT, power_draw REAL, power_limit REAL, power_state TEXT, memory_used INTEGER, memory_total INTEGER, memory_free INTEGER, gpu_utilization INTEGER, memory_utilization INTEGER, fan_speed INTEGER, persistence_mode TEXT, compute_mode TEXT, mig_mode TEXT, ecc_mode TEXT, pci_link_gen TEXT, pci_link_width TEXT, clock_graphics TEXT, clock_memory TEXT, -- RMA 관련 remap_corr_error INTEGER DEFAULT 0, remap_uncorr_error INTEGER DEFAULT 0, remap_pending TEXT, remap_failure TEXT, remap_bank_max INTEGER DEFAULT 0, remap_bank_high INTEGER DEFAULT 0, remap_bank_partial INTEGER DEFAULT 0, remap_bank_low INTEGER DEFAULT 0, remap_bank_none INTEGER DEFAULT 0, sram_corr_volatile TEXT, sram_uncorr_parity_volatile TEXT, sram_uncorr_secded_volatile TEXT, sram_corr_aggregate TEXT, sram_uncorr_parity_aggregate TEXT, sram_uncorr_secded_aggregate TEXT, sram_threshold_exceeded TEXT, sram_l2 TEXT, sram_sm TEXT, sram_microcontroller TEXT, sram_pcie TEXT, sram_other TEXT, dram_corr_volatile TEXT, dram_uncorr_volatile TEXT, dram_corr_aggregate TEXT, dram_uncorr_aggregate TEXT, -- HW Slowdown hw_slowdown TEXT, hw_thermal_slowdown TEXT, hw_power_brake_slowdown TEXT ); CREATE TABLE IF NOT EXISTS analysis_rma ( id INTEGER PRIMARY KEY AUTOINCREMENT, analysis_id INTEGER NOT NULL REFERENCES analysis(id) ON DELETE CASCADE, gpu_index INTEGER, gpu_name TEXT, serial_number TEXT, qualifies_for_rma INTEGER DEFAULT 0, reasons TEXT, warnings TEXT, row_remap_details TEXT, sram_details TEXT ); CREATE TABLE IF NOT EXISTS analysis_xid ( id INTEGER PRIMARY KEY AUTOINCREMENT, analysis_id INTEGER NOT NULL REFERENCES analysis(id) ON DELETE CASCADE, timestamp TEXT, xid_code INTEGER, xid_name TEXT, severity TEXT, pci_address TEXT, gpu_name TEXT, gpu_serial TEXT, description TEXT, action TEXT, raw_line TEXT ); CREATE TABLE IF NOT EXISTS analysis_kernel ( id INTEGER PRIMARY KEY AUTOINCREMENT, analysis_id INTEGER NOT NULL REFERENCES analysis(id) ON DELETE CASCADE, timestamp TEXT, level TEXT, message TEXT, raw_line TEXT ); CREATE INDEX IF NOT EXISTS idx_analysis_created ON analysis(created_at); CREATE INDEX IF NOT EXISTS idx_analysis_status ON analysis(overall_status); CREATE INDEX IF NOT EXISTS idx_xid_code ON analysis_xid(xid_code); CREATE INDEX IF NOT EXISTS idx_xid_severity ON analysis_xid(severity); CREATE INDEX IF NOT EXISTS idx_gpu_serial ON analysis_gpu(serial_number); CREATE INDEX IF NOT EXISTS idx_kernel_level ON analysis_kernel(level); CREATE INDEX IF NOT EXISTS idx_rma_serial ON analysis_rma(serial_number); CREATE INDEX IF NOT EXISTS idx_rma_qualifies ON analysis_rma(qualifies_for_rma); """) # 기존 DB 마이그레이션: gpu_name 컬럼이 없으면 추가 cols = {row[1] for row in conn.execute("PRAGMA table_info(analysis_xid)")} if "gpu_name" not in cols: conn.execute("ALTER TABLE analysis_xid ADD COLUMN gpu_name TEXT") if "gpu_serial" not in cols: conn.execute("ALTER TABLE analysis_xid ADD COLUMN gpu_serial TEXT") # HW slowdown 마이그레이션 gpu_cols = {row[1] for row in conn.execute("PRAGMA table_info(analysis_gpu)")} if "hw_slowdown" not in gpu_cols: conn.execute("ALTER TABLE analysis_gpu ADD COLUMN hw_slowdown TEXT") if "hw_thermal_slowdown" not in gpu_cols: conn.execute("ALTER TABLE analysis_gpu ADD COLUMN hw_thermal_slowdown TEXT") if "hw_power_brake_slowdown" not in gpu_cols: conn.execute("ALTER TABLE analysis_gpu ADD COLUMN hw_power_brake_slowdown TEXT") def save_analysis(result) -> int: """분석 결과 전체를 DB에 저장하고 analysis_id 반환""" with get_conn() as conn: cur = conn.execute(""" INSERT INTO analysis (filename, file_size_mb, overall_status, total_issues, critical_count, warning_count, info_count, driver_version, cuda_version, gpu_count, xid_count, kernel_event_count, rma_count, ecc_info) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, ( result.filename, result.file_size_mb, result.overall_status, result.total_issues, result.critical_count, result.warning_count, result.info_count, result.driver_version, result.cuda_version, len(result.gpus), len(result.xid_events), len(result.kernel_events), result.rma_count, json.dumps(result.ecc_info, ensure_ascii=False) if result.ecc_info else "{}", )) analysis_id = cur.lastrowid # GPU 정보 저장 for gpu in result.gpus: conn.execute(""" INSERT INTO analysis_gpu (analysis_id, gpu_index, name, serial_number, gpu_uuid, pci_address, product_brand, product_architecture, driver_version, cuda_version, vbios_version, inforom_version, temperature, temperature_limit, power_draw, power_limit, power_state, memory_used, memory_total, memory_free, gpu_utilization, memory_utilization, fan_speed, persistence_mode, compute_mode, mig_mode, ecc_mode, pci_link_gen, pci_link_width, clock_graphics, clock_memory, remap_corr_error, remap_uncorr_error, remap_pending, remap_failure, remap_bank_max, remap_bank_high, remap_bank_partial, remap_bank_low, remap_bank_none, sram_corr_volatile, sram_uncorr_parity_volatile, sram_uncorr_secded_volatile, sram_corr_aggregate, sram_uncorr_parity_aggregate, sram_uncorr_secded_aggregate, sram_threshold_exceeded, sram_l2, sram_sm, sram_microcontroller, sram_pcie, sram_other, dram_corr_volatile, dram_uncorr_volatile, dram_corr_aggregate, dram_uncorr_aggregate, hw_slowdown, hw_thermal_slowdown, hw_power_brake_slowdown) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, ( analysis_id, gpu.index, gpu.name, gpu.serial_number, gpu.gpu_uuid, gpu.pci_address, gpu.product_brand, gpu.product_architecture, gpu.driver_version, gpu.cuda_version, gpu.vbios_version, gpu.inforom_version, gpu.temperature, gpu.temperature_limit, gpu.power_draw, gpu.power_limit, gpu.power_state, gpu.memory_used, gpu.memory_total, gpu.memory_free, gpu.gpu_utilization, gpu.memory_utilization, gpu.fan_speed, gpu.persistence_mode, gpu.compute_mode, gpu.mig_mode, gpu.ecc_mode, gpu.pci_link_gen_current, gpu.pci_link_width_current, gpu.clock_graphics, gpu.clock_memory, gpu.remap_corr_error, gpu.remap_uncorr_error, gpu.remap_pending, gpu.remap_failure, gpu.remap_bank_max, gpu.remap_bank_high, gpu.remap_bank_partial, gpu.remap_bank_low, gpu.remap_bank_none, gpu.sram_corr_volatile, gpu.sram_uncorr_parity_volatile, gpu.sram_uncorr_secded_volatile, gpu.sram_corr_aggregate, gpu.sram_uncorr_parity_aggregate, gpu.sram_uncorr_secded_aggregate, gpu.sram_threshold_exceeded, gpu.sram_l2, gpu.sram_sm, gpu.sram_microcontroller, gpu.sram_pcie, gpu.sram_other, gpu.dram_corr_volatile, gpu.dram_uncorr_volatile, gpu.dram_corr_aggregate, gpu.dram_uncorr_aggregate, gpu.hw_slowdown, gpu.hw_thermal_slowdown, gpu.hw_power_brake_slowdown, )) # XID 이벤트 저장 for xid in result.xid_events: conn.execute(""" INSERT INTO analysis_xid (analysis_id, timestamp, xid_code, xid_name, severity, pci_address, gpu_name, gpu_serial, description, action, raw_line) VALUES (?,?,?,?,?,?,?,?,?,?,?) """, ( analysis_id, xid.timestamp, xid.xid_code, xid.name, xid.severity, xid.pci_address, xid.gpu_name, xid.gpu_serial, xid.description, xid.action, xid.raw_line, )) # 커널 이벤트 저장 for ke in result.kernel_events: conn.execute(""" INSERT INTO analysis_kernel (analysis_id, timestamp, level, message, raw_line) VALUES (?,?,?,?,?) """, ( analysis_id, ke.timestamp, ke.level, ke.message, ke.raw_line, )) # RMA 판정 결과 저장 for v in result.rma_verdicts: conn.execute(""" INSERT INTO analysis_rma (analysis_id, gpu_index, gpu_name, serial_number, qualifies_for_rma, reasons, warnings, row_remap_details, sram_details) VALUES (?,?,?,?,?,?,?,?,?) """, ( analysis_id, v.gpu_index, v.gpu_name, v.serial_number, 1 if v.qualifies_for_rma else 0, json.dumps(v.reasons, ensure_ascii=False), json.dumps(v.warnings, ensure_ascii=False), json.dumps(v.row_remap_details, ensure_ascii=False), json.dumps(v.sram_details, ensure_ascii=False), )) return analysis_id def update_memo(analysis_id: int, memo: str): """분석 건에 메모 추가/수정""" with get_conn() as conn: conn.execute("UPDATE analysis SET memo = ? WHERE id = ?", (memo, analysis_id)) def delete_analysis(analysis_id: int): """분석 건 삭제 (CASCADE로 하위 테이블도 삭제)""" with get_conn() as conn: conn.execute("DELETE FROM analysis WHERE id = ?", (analysis_id,)) def get_analysis_list(limit=100, status_filter=None, search=None): """분석 이력 목록 조회""" with get_conn() as conn: query = "SELECT * FROM analysis WHERE 1=1" params = [] if status_filter and status_filter != "전체": query += " AND overall_status = ?" params.append(status_filter) if search: query += " AND (filename LIKE ? OR memo LIKE ? OR driver_version LIKE ?)" params.extend([f"%{search}%", f"%{search}%", f"%{search}%"]) query += " ORDER BY created_at DESC LIMIT ?" params.append(limit) return [dict(row) for row in conn.execute(query, params).fetchall()] def get_analysis_detail(analysis_id: int): """분석 건 상세 조회 (GPU, XID, 커널 이벤트 포함)""" with get_conn() as conn: analysis = conn.execute("SELECT * FROM analysis WHERE id = ?", (analysis_id,)).fetchone() if not analysis: return None result = dict(analysis) result["gpus"] = [dict(r) for r in conn.execute( "SELECT * FROM analysis_gpu WHERE analysis_id = ? ORDER BY gpu_index", (analysis_id,) ).fetchall()] result["xid_events"] = [dict(r) for r in conn.execute( "SELECT * FROM analysis_xid WHERE analysis_id = ? ORDER BY id", (analysis_id,) ).fetchall()] result["kernel_events"] = [dict(r) for r in conn.execute( "SELECT * FROM analysis_kernel WHERE analysis_id = ? ORDER BY id", (analysis_id,) ).fetchall()] rma_rows = conn.execute( "SELECT * FROM analysis_rma WHERE analysis_id = ? ORDER BY gpu_index", (analysis_id,) ).fetchall() result["rma_verdicts"] = [] for r in rma_rows: d = dict(r) d["reasons"] = json.loads(d.get("reasons") or "[]") d["warnings"] = json.loads(d.get("warnings") or "[]") d["row_remap_details"] = json.loads(d.get("row_remap_details") or "{}") d["sram_details"] = json.loads(d.get("sram_details") or "{}") result["rma_verdicts"].append(d) return result def get_xid_statistics(): """전체 이력에서 XID 코드별 발생 통계""" with get_conn() as conn: return [dict(r) for r in conn.execute(""" SELECT xid_code, xid_name, severity, COUNT(*) as total_count, COUNT(DISTINCT analysis_id) as affected_reports, MIN(a.created_at) as first_seen, MAX(a.created_at) as last_seen FROM analysis_xid x JOIN analysis a ON a.id = x.analysis_id GROUP BY xid_code ORDER BY total_count DESC """).fetchall()] def get_gpu_serial_history(serial_number: str): """특정 GPU 시리얼 번호의 장애 이력 추적""" with get_conn() as conn: return [dict(r) for r in conn.execute(""" SELECT a.id, a.created_at, a.filename, a.overall_status, a.total_issues, a.critical_count, a.memo, g.temperature, g.power_draw, g.memory_used, g.memory_total FROM analysis_gpu g JOIN analysis a ON a.id = g.analysis_id WHERE g.serial_number = ? ORDER BY a.created_at DESC """, (serial_number,)).fetchall()] def get_all_gpu_serials(): """DB에 기록된 모든 GPU 시리얼 번호 목록""" with get_conn() as conn: return [dict(r) for r in conn.execute(""" SELECT DISTINCT serial_number, name, gpu_uuid, COUNT(DISTINCT analysis_id) as report_count FROM analysis_gpu WHERE serial_number != 'N/A' GROUP BY serial_number ORDER BY report_count DESC """).fetchall()] def get_status_summary(): """전체 분석 이력 상태 요약 (대시보드용)""" with get_conn() as conn: total = conn.execute("SELECT COUNT(*) as cnt FROM analysis").fetchone()["cnt"] by_status = {r["overall_status"]: r["cnt"] for r in conn.execute( "SELECT overall_status, COUNT(*) as cnt FROM analysis GROUP BY overall_status" ).fetchall()} recent_critical = [dict(r) for r in conn.execute(""" SELECT id, created_at, filename, critical_count, memo FROM analysis WHERE overall_status = 'critical' ORDER BY created_at DESC LIMIT 5 """).fetchall()] return { "total_reports": total, "by_status": by_status, "recent_critical": recent_critical, }