""" NV-Log Analyzer - Streamlit 대시보드 nvidia-bug-report.log.gz 파일을 업로드하면 즉시 분석 + DB 저장. 이력 조회, GPU 시리얼 추적, XID 통계 등 케이스 축적 기능 포함. """ import streamlit as st import pandas as pd import json from pathlib import Path from log_parser import analyze_log, read_log_file, XID_DB from db import init_db, save_analysis, update_memo, delete_analysis from db import get_analysis_list, get_analysis_detail from db import get_xid_statistics, get_gpu_serial_history, get_all_gpu_serials, get_status_summary from auth import ( init_user_db, authenticate, register_user, get_user_permissions, get_all_users, get_pending_users, approve_user, reject_user, update_user_role, delete_user, update_password, ROLES, ) # --- DB 초기화 (앱 시작 시 1회) --- init_db() init_user_db() # --- 페이지 설정 --- st.set_page_config( page_title="NV-Log Analyzer", page_icon="🖥️", layout="wide", initial_sidebar_state="expanded", ) # --- 커스텀 CSS --- st.markdown(""" """, unsafe_allow_html=True) # ============================================= # 인증 헬퍼 # ============================================= def get_current_user(): """현재 로그인된 사용자 정보 반환. 없으면 None""" return st.session_state.get("user") def get_perms(): """현재 사용자 권한 dict""" user = get_current_user() if not user: return ROLES["viewer"] return get_user_permissions(user["role"]) def require_login(): """로그인 안 되었으면 로그인 페이지 표시 후 True 반환 (= 중단 필요)""" if get_current_user(): return False page_login() return True # ============================================= # 로그인 / 회원가입 페이지 # ============================================= def page_login(): # 고정 너비 중앙 정렬 col_left, col_center, col_right = st.columns([1, 2, 1]) with col_center: st.markdown('

🖥️ NV-Log Analyzer

', unsafe_allow_html=True) tab_login, tab_register = st.tabs(["🔑 로그인", "📝 회원가입"]) with tab_login: with st.form("login_form"): username = st.text_input("아이디") password = st.text_input("비밀번호", type="password") submitted = st.form_submit_button("로그인", use_container_width=True) if submitted and username and password: result = authenticate(username, password) if result is None: st.error("아이디 또는 비밀번호가 올바르지 않습니다.") elif "error" in result: if result["status"] == "pending": st.warning("가입 승인 대기 중입니다. 관리자 승인 후 로그인할 수 있습니다.") elif result["status"] == "rejected": st.error("가입이 거절되었습니다. 관리자에게 문의하세요.") else: st.session_state["user"] = result st.rerun() with tab_register: with st.form("register_form"): new_username = st.text_input("아이디 (3자 이상)") new_display = st.text_input("표시 이름") new_password = st.text_input("비밀번호 (6자 이상)", type="password") new_password2 = st.text_input("비밀번호 확인", type="password") reg_submitted = st.form_submit_button("회원가입 신청", use_container_width=True) if reg_submitted: if new_password != new_password2: st.error("비밀번호가 일치하지 않습니다.") elif new_username and new_password: res = register_user(new_username, new_password, new_display or new_username) if res["success"]: st.success("회원가입이 신청되었습니다. 관리자 승인 후 로그인할 수 있습니다.") else: st.error(res["error"]) st.markdown("---") st.caption("기본 관리자: admin / admin1234 (최초 로그인 후 반드시 비밀번호를 변경하세요)") # ============================================= # 페이지 5: 사용자 관리 (admin 전용) # ============================================= def page_user_management(): st.markdown('

👤 사용자 관리

', unsafe_allow_html=True) st.markdown('

사용자 가입 승인, 역할 변경, 계정 관리를 수행합니다.

', unsafe_allow_html=True) user = get_current_user() if not user or user["role"] != "admin": st.error("관리자만 접근할 수 있습니다.") return # 승인 대기 사용자 pending = get_pending_users() if pending: st.subheader(f"🔔 승인 대기 ({len(pending)}명)") for p in pending: with st.container(): pc1, pc2, pc3, pc4 = st.columns([2, 2, 2, 2]) pc1.write(f"**{p['display_name']}** (`{p['username']}`)") pc2.write(f"가입 요청: {p['created_at']}") with pc3: role_choice = st.selectbox( "역할", list(ROLES.keys()), index=2, # default viewer key=f"role_{p['id']}", format_func=lambda r: f"{ROLES[r]['label']} - {ROLES[r]['description'][:20]}", ) with pc4: bc1, bc2 = st.columns(2) if bc1.button("✅ 승인", key=f"approve_{p['id']}"): approve_user(p["id"], role_choice, user["username"]) st.success(f"{p['username']} 승인 완료 ({ROLES[role_choice]['label']})") st.rerun() if bc2.button("❌ 거절", key=f"reject_{p['id']}"): reject_user(p["id"]) st.warning(f"{p['username']} 거절됨") st.rerun() st.markdown("---") else: st.info("승인 대기 중인 사용자가 없습니다.") # 전체 사용자 목록 st.subheader("📋 전체 사용자") all_users = get_all_users() status_emoji = {"approved": "🟢", "pending": "🟡", "rejected": "🔴"} user_table = [{ "ID": u["id"], "아이디": u["username"], "이름": u["display_name"], "역할": f"{ROLES.get(u['role'], {}).get('label', u['role'])}", "상태": f"{status_emoji.get(u['status'], '')} {u['status']}", "가입일": u["created_at"], "승인자": u.get("approved_by") or "-", "최근 로그인": u.get("last_login") or "-", } for u in all_users] st.dataframe(pd.DataFrame(user_table), use_container_width=True, hide_index=True) # 개별 사용자 관리 st.markdown("---") st.subheader("🔧 사용자 설정 변경") manageable = [u for u in all_users if u["username"] != "admin"] if not manageable: st.info("관리할 사용자가 없습니다.") return selected_user = st.selectbox( "사용자 선택", manageable, format_func=lambda u: f"{u['display_name']} ({u['username']}) - {ROLES.get(u['role'], {}).get('label', u['role'])}", ) if selected_user: mc1, mc2, mc3 = st.columns(3) with mc1: new_role = st.selectbox( "역할 변경", list(ROLES.keys()), index=list(ROLES.keys()).index(selected_user["role"]), key="change_role", format_func=lambda r: f"{ROLES[r]['label']}", ) if st.button("역할 적용"): update_user_role(selected_user["id"], new_role) st.success(f"{selected_user['username']}의 역할이 {ROLES[new_role]['label']}(으)로 변경되었습니다.") st.rerun() with mc2: new_pw = st.text_input("새 비밀번호 (6자 이상)", type="password", key="reset_pw") if st.button("비밀번호 초기화"): if new_pw and len(new_pw) >= 6: update_password(selected_user["id"], new_pw) st.success(f"{selected_user['username']}의 비밀번호가 변경되었습니다.") else: st.error("비밀번호는 6자 이상이어야 합니다.") with mc3: st.write("") st.write("") if st.button("🗑️ 계정 삭제", type="secondary"): delete_user(selected_user["id"]) st.success(f"{selected_user['username']} 계정이 삭제되었습니다.") st.rerun() # 역할별 권한 안내 st.markdown("---") with st.expander("📖 역할별 권한 안내"): for role_key, role_info in ROLES.items(): perms = [] if role_info["can_upload"]: perms.append("로그 업로드") if role_info["can_view"]: perms.append("조회") if role_info["can_edit_memo"]: perms.append("메모 수정") if role_info["can_delete"]: perms.append("삭제") if role_info["can_manage_users"]: perms.append("사용자 관리") st.write(f"**{role_info['label']}** ({role_key}): {', '.join(perms)}") # ============================================= # 공통 렌더링 함수 # ============================================= def render_status_badge(status): status_map = { "critical": ("🔴 심각", "status-critical", "즉각적인 조치가 필요합니다. 하드웨어 손상 또는 서비스 중단 위험이 감지되었습니다."), "warning": ("🟡 경고", "status-warning", "주의가 필요한 항목이 발견되었습니다. 방치 시 문제가 악화될 수 있습니다."), "healthy": ("🟢 정상", "status-healthy", "심각한 문제가 감지되지 않았습니다."), } label, css_class, desc = status_map.get(status, status_map["healthy"]) st.markdown(f'

{label}

' f'

{desc}

', unsafe_allow_html=True) def render_gpu_details_from_result(gpus): """GPU 상세 (AnalysisResult.gpus 객체 리스트용)""" if not gpus: st.info("GPU 정보를 로그에서 추출하지 못했습니다.") return for gpu in gpus: with st.expander(f"GPU {gpu.index}: {gpu.name}", expanded=True): col1, col2, col3, col4 = st.columns(4) temp_delta = "정상" if gpu.temperature < 85 else "과열 주의!" col1.metric("온도", f"{gpu.temperature}°C", delta=temp_delta, delta_color="off" if gpu.temperature < 85 else "inverse") if gpu.power_limit > 0: col2.metric("전력", f"{gpu.power_draw}W / {gpu.power_limit}W", delta=f"{round(gpu.power_draw/gpu.power_limit*100)}%") else: col2.metric("전력", f"{gpu.power_draw}W") if gpu.memory_total > 0: col3.metric("메모리", f"{gpu.memory_used} / {gpu.memory_total} MiB", delta=f"{round(gpu.memory_used/gpu.memory_total*100)}%") else: col3.metric("메모리", "N/A") col4.metric("GPU 사용률", f"{gpu.gpu_utilization}%") spec_col1, spec_col2 = st.columns(2) with spec_col1: for k, v in {"제품명": gpu.name, "브랜드": gpu.product_brand, "아키텍처": gpu.product_architecture, "시리얼 번호": gpu.serial_number, "GPU UUID": gpu.gpu_uuid, "VBIOS": gpu.vbios_version, "InfoROM": gpu.inforom_version}.items(): if v and v != "N/A": st.write(f"**{k}:** `{v}`") with spec_col2: pcie = f"{gpu.pci_link_gen_current} x{gpu.pci_link_width_current}" if gpu.pci_link_gen_current != "N/A" else "N/A" for k, v in {"PCI 주소": gpu.pci_address, "PCIe 링크": pcie, "Power State": gpu.power_state, "팬 속도": f"{gpu.fan_speed}%" if gpu.fan_speed > 0 else "N/A", "Persistence": gpu.persistence_mode, "Compute Mode": gpu.compute_mode, "MIG Mode": gpu.mig_mode, "ECC Mode": gpu.ecc_mode}.items(): if v and v != "N/A": st.write(f"**{k}:** `{v}`") if gpu.clock_graphics != "N/A" or gpu.clock_memory != "N/A": st.markdown("**클럭 정보**") cc1, cc2 = st.columns(2) if gpu.clock_graphics != "N/A": cc1.write(f"**Graphics:** `{gpu.clock_graphics}`") if gpu.clock_memory != "N/A": cc2.write(f"**Memory:** `{gpu.clock_memory}`") if gpu.hw_slowdown != "N/A": hw_sd = gpu.hw_slowdown hw_th = gpu.hw_thermal_slowdown hw_pb = gpu.hw_power_brake_slowdown any_active = hw_sd == "Active" or hw_th == "Active" or hw_pb == "Active" label = "⚠️ **HW Slowdown 상태**" if any_active else "**HW Slowdown 상태**" st.markdown(label) sd1, sd2, sd3 = st.columns(3) sd1.metric("HW Slowdown", hw_sd, delta="Active" if hw_sd == "Active" else None, delta_color="inverse") if hw_th != "N/A": sd2.metric("HW Thermal Slowdown", hw_th, delta="Active" if hw_th == "Active" else None, delta_color="inverse") if hw_pb != "N/A": sd3.metric("HW Power Brake Slowdown", hw_pb, delta="Active" if hw_pb == "Active" else None, delta_color="inverse") def render_gpu_details_from_dict(gpus): """GPU 상세 (DB dict 리스트용)""" if not gpus: st.info("GPU 정보 없음") return for gpu in gpus: with st.expander(f"GPU {gpu['gpu_index']}: {gpu['name']}", expanded=True): col1, col2, col3, col4 = st.columns(4) temp = gpu.get("temperature", 0) or 0 col1.metric("온도", f"{temp}°C", delta="정상" if temp < 85 else "과열 주의!", delta_color="off" if temp < 85 else "inverse") pw = gpu.get("power_draw", 0) or 0 pl = gpu.get("power_limit", 0) or 0 if pl > 0: col2.metric("전력", f"{pw}W / {pl}W", delta=f"{round(pw/pl*100)}%") else: col2.metric("전력", f"{pw}W") mu = gpu.get("memory_used", 0) or 0 mt = gpu.get("memory_total", 0) or 0 if mt > 0: col3.metric("메모리", f"{mu} / {mt} MiB", delta=f"{round(mu/mt*100)}%") else: col3.metric("메모리", "N/A") col4.metric("GPU 사용률", f"{gpu.get('gpu_utilization', 0)}%") spec_col1, spec_col2 = st.columns(2) with spec_col1: for k, field in [("제품명", "name"), ("브랜드", "product_brand"), ("아키텍처", "product_architecture"), ("시리얼 번호", "serial_number"), ("GPU UUID", "gpu_uuid"), ("VBIOS", "vbios_version"), ("InfoROM", "inforom_version")]: v = gpu.get(field, "N/A") or "N/A" if v != "N/A": st.write(f"**{k}:** `{v}`") with spec_col2: gen = gpu.get("pci_link_gen", "N/A") or "N/A" width = gpu.get("pci_link_width", "N/A") or "N/A" pcie = f"{gen} x{width}" if gen != "N/A" else "N/A" fan = gpu.get("fan_speed", 0) or 0 for k, v in [("PCI 주소", gpu.get("pci_address", "N/A")), ("PCIe 링크", pcie), ("Power State", gpu.get("power_state", "N/A")), ("팬 속도", f"{fan}%" if fan > 0 else "N/A"), ("Persistence", gpu.get("persistence_mode", "N/A")), ("Compute Mode", gpu.get("compute_mode", "N/A")), ("MIG Mode", gpu.get("mig_mode", "N/A")), ("ECC Mode", gpu.get("ecc_mode", "N/A"))]: if v and v != "N/A": st.write(f"**{k}:** `{v}`") hw_sd = gpu.get("hw_slowdown", "N/A") or "N/A" if hw_sd != "N/A": hw_th = gpu.get("hw_thermal_slowdown", "N/A") or "N/A" hw_pb = gpu.get("hw_power_brake_slowdown", "N/A") or "N/A" any_active = hw_sd == "Active" or hw_th == "Active" or hw_pb == "Active" label = "⚠️ **HW Slowdown 상태**" if any_active else "**HW Slowdown 상태**" st.markdown(label) sd1, sd2, sd3 = st.columns(3) sd1.metric("HW Slowdown", hw_sd, delta="Active" if hw_sd == "Active" else None, delta_color="inverse") if hw_th != "N/A": sd2.metric("HW Thermal Slowdown", hw_th, delta="Active" if hw_th == "Active" else None, delta_color="inverse") if hw_pb != "N/A": sd3.metric("HW Power Brake Slowdown", hw_pb, delta="Active" if hw_pb == "Active" else None, delta_color="inverse") def render_rma_section(rma_verdicts, source="result"): """RMA 판정 결과 섹션 (AnalysisResult.rma_verdicts 또는 DB dict 리스트 공용)""" st.subheader("🔧 RMA 적격 판정 (NVIDIA Hopper 기준)") if not rma_verdicts: st.info("RMA 판정 데이터가 없습니다.") return rma_qualified = [] rma_warnings = [] rma_clean = [] for v in rma_verdicts: if source == "result": # AnalysisResult.rma_verdicts (RmaVerdict 객체) qualifies = v.qualifies_for_rma idx = v.gpu_index name = v.gpu_name serial = v.serial_number reasons = v.reasons warnings = v.warnings remap = v.row_remap_details sram = v.sram_details else: # DB dict qualifies = v.get("qualifies_for_rma", 0) == 1 idx = v.get("gpu_index", 0) name = v.get("gpu_name", "N/A") serial = v.get("serial_number", "N/A") reasons = v.get("reasons", []) warnings = v.get("warnings", []) remap = v.get("row_remap_details", {}) sram = v.get("sram_details", {}) entry = { "idx": idx, "name": name, "serial": serial, "qualifies": qualifies, "reasons": reasons, "warnings": warnings, "remap": remap, "sram": sram, } if qualifies: rma_qualified.append(entry) elif warnings: rma_warnings.append(entry) else: rma_clean.append(entry) # RMA 대상 GPU 요약 if rma_qualified: st.markdown(f"""

🔴 RMA 대상 GPU: {len(rma_qualified)}개

아래 GPU는 NVIDIA RMA 기준에 해당합니다. 벤더사에 교체를 요청하세요.

""", unsafe_allow_html=True) elif rma_warnings: st.markdown(f"""

🟡 주의 필요 GPU: {len(rma_warnings)}개

아직 RMA 기준 미달이나 모니터링이 필요한 GPU가 있습니다.

""", unsafe_allow_html=True) else: st.markdown(f"""

🟢 RMA 해당 없음

모든 GPU가 RMA 기준 내 정상 범위입니다.

""", unsafe_allow_html=True) # GPU별 상세 for entry in rma_qualified + rma_warnings + rma_clean: status_icon = "🔴" if entry["qualifies"] else ("🟡" if entry["warnings"] else "🟢") status_text = "RMA 대상" if entry["qualifies"] else ("주의 필요" if entry["warnings"] else "정상") with st.expander( f"{status_icon} GPU {entry['idx']}: {entry['name']} (S/N: {entry['serial']}) — {status_text}", expanded=entry["qualifies"] ): # RMA 사유 if entry["reasons"]: st.markdown("**🔴 RMA 사유:**") for r in entry["reasons"]: st.error(r) # 경고 if entry["warnings"]: st.markdown("**🟡 경고:**") for w in entry["warnings"]: st.warning(w) # Row Remapping 상세 remap = entry["remap"] if remap: st.markdown("**Row Remapping 상태:**") rc1, rc2, rc3, rc4 = st.columns(4) rc1.metric("Correctable 리매핑", remap.get("correctable_error", 0)) rc2.metric("Uncorrectable 리매핑", remap.get("uncorrectable_error", 0)) rc3.metric("리매핑 대기", remap.get("pending", "N/A")) rc4.metric("리매핑 실패", remap.get("failure_occurred", "N/A")) st.markdown("**Bank Remap 가용성:**") bc1, bc2, bc3, bc4, bc5 = st.columns(5) bc1.metric("Max", f"{remap.get('bank_max', 0)} banks") bc2.metric("High", f"{remap.get('bank_high', 0)} banks") bc3.metric("Partial", f"{remap.get('bank_partial', 0)} banks") bc4.metric("Low", f"{remap.get('bank_low', 0)} banks") bc5.metric("None", f"{remap.get('bank_none', 0)} banks") # SRAM 상세 sram = entry["sram"] if sram: st.markdown("**SRAM ECC 상태:**") agg = sram.get("aggregate", {}) vol = sram.get("volatile", {}) dram = sram.get("dram", {}) sc1, sc2, sc3 = st.columns(3) with sc1: st.markdown("*Aggregate (누적)*") st.write(f"Correctable: `{agg.get('correctable', 'N/A')}`") st.write(f"Parity UCE: `{agg.get('uncorrectable_parity', 'N/A')}`") st.write(f"SECDED UCE: `{agg.get('uncorrectable_secded', 'N/A')}`") threshold = agg.get("threshold_exceeded", "N/A") if threshold.lower() == "yes": st.write(f"**Threshold Exceeded: `{threshold}`** ← RMA 대상") else: st.write(f"Threshold Exceeded: `{threshold}`") with sc2: st.markdown("*Volatile (휘발성)*") st.write(f"Correctable: `{vol.get('correctable', 'N/A')}`") st.write(f"Parity UCE: `{vol.get('uncorrectable_parity', 'N/A')}`") st.write(f"SECDED UCE: `{vol.get('uncorrectable_secded', 'N/A')}`") with sc3: st.markdown("*DRAM*") st.write(f"Corr (Vol): `{dram.get('corr_volatile', 'N/A')}`") st.write(f"Uncorr (Vol): `{dram.get('uncorr_volatile', 'N/A')}`") st.write(f"Corr (Agg): `{dram.get('corr_aggregate', 'N/A')}`") st.write(f"Uncorr (Agg): `{dram.get('uncorr_aggregate', 'N/A')}`") # SRAM Sources sources = sram.get("sources", {}) if any(v != "N/A" for v in sources.values()): st.markdown("**Uncorrectable SRAM Sources:**") ss1, ss2, ss3, ss4, ss5 = st.columns(5) ss1.write(f"L2: `{sources.get('l2', 'N/A')}`") ss2.write(f"SM: `{sources.get('sm', 'N/A')}`") ss3.write(f"MC: `{sources.get('microcontroller', 'N/A')}`") ss4.write(f"PCIe: `{sources.get('pcie', 'N/A')}`") ss5.write(f"Other: `{sources.get('other', 'N/A')}`") if not entry["qualifies"] and not entry["warnings"]: st.success("Row-Remapping, SRAM 모두 정상 범위입니다.") # RMA 기준 안내 with st.expander("📖 NVIDIA Hopper RMA 기준 안내", expanded=False): st.markdown(""" **Row-Remapping RMA 기준 (DRAM)** - `Remapping Failure Occurred = Yes` → 즉시 RMA - 한 Bank에서 Uncorrectable Error Row 리매핑 **8회 이상** - 총 리매핑 **512회** 이상 - 8회 리매핑 후 Field Diagnostics에서 문제 감지 **SRAM RMA 기준 (L2 Cache)** - `SRAM Threshold Exceeded = Yes` → 즉시 RMA - Parity 보호 SRAM: 주소 뱅크 내 **UCE 4개** 이상 - SECDED ECC 보호 SRAM: 주소 뱅크 내 **UCE 2개** 이상 > 참고: DRAM은 별도 RMA 기준 없음. NVIDIA Field Diagnostic 도구로 최종 확인 필요. > [NVIDIA 공식 문서](https://docs.nvidia.com/deploy/a100-gpu-mem-error-mgmt/rma-policy-thresholds-for-row-remapping.html) """) # ============================================= # 페이지 1: 로그 분석 (메인) # ============================================= def page_analyze(): st.markdown('

🖥️ NV-Log Analyzer

', unsafe_allow_html=True) st.markdown('

nvidia-bug-report.log.gz 파일을 업로드하여 GPU 상태를 즉시 분석하고 DB에 저장합니다.

', unsafe_allow_html=True) perms = get_perms() if not perms["can_upload"]: st.warning("로그 업로드 권한이 없습니다. 관리자에게 문의하세요.") st.info("분석 이력 조회는 '📚 분석 이력' 메뉴에서 가능합니다.") return uploaded_file = st.file_uploader( "nvidia-bug-report.log.gz 또는 .log 파일 업로드", type=["gz", "log", "txt"], help="nvidia-bug-report.sh가 생성한 로그 파일을 업로드하세요.", ) if uploaded_file is None: st.markdown("---") col1, col2, col3 = st.columns(3) with col1: st.markdown("#### 🔍 XID 에러 탐지\n`NVRM: Xid` 패턴을 자동 검출하고, 에러 코드별 의미와 권장 조치를 제공합니다.") with col2: st.markdown("#### 🌡️ GPU 헬스 체크\n온도, 전력, ECC 에러, 메모리 상태를 한눈에 확인할 수 있습니다.") with col3: st.markdown("#### 🐧 커널 로그 필터링\ndmesg 내 Critical, Error, OOM 등 핵심 이벤트만 추출합니다.") st.info("위에서 nvidia-bug-report.log.gz 파일을 업로드하세요.") return # 분석 실행 with st.spinner("🔄 로그 분석 중..."): file_content = uploaded_file.read() result = analyze_log(file_content, uploaded_file.name) # DB 저장 (중복 방지: 같은 파일명+크기가 이미 있으면 스킵) existing = get_analysis_list(limit=1, search=uploaded_file.name) if existing and existing[0]["file_size_mb"] == result.file_size_mb: analysis_id = existing[0]["id"] st.info(f"이미 저장된 분석 결과입니다. (ID: {analysis_id})") else: analysis_id = save_analysis(result) st.success(f"분석 결과가 DB에 저장되었습니다. (ID: {analysis_id})") # 메모 입력 (editor/admin만) if perms["can_edit_memo"]: memo = st.text_input("📝 이 분석에 대한 메모 (장애 상황, 조치 내용 등)", key="memo_input", placeholder="예: Dell 서버 R750xa, GPU 4번 XID 61 반복 발생으로 RMA 접수") if memo: update_memo(analysis_id, memo) st.caption("메모가 저장되었습니다.") # --- 결과 출력 --- st.markdown("---") render_status_badge(result.overall_status) col1, col2, col3, col4, col5, col6 = st.columns(6) col1.metric("전체 이슈", result.total_issues) col2.metric("심각", result.critical_count) col3.metric("경고", result.warning_count) col4.metric("정보", result.info_count) col5.metric("GPU 수", len(result.gpus)) col6.metric("🔧 RMA 대상", result.rma_count) st.markdown("---") st.subheader("📋 시스템 정보") ic1, ic2, ic3, ic4 = st.columns(4) ic1.metric("파일명", result.filename[:30] + ("..." if len(result.filename) > 30 else "")) ic2.metric("파일 크기", f"{result.file_size_mb} MB") ic3.metric("드라이버", result.driver_version) ic4.metric("CUDA", result.cuda_version) st.markdown("---") st.subheader("🎮 GPU 상태") render_gpu_details_from_result(result.gpus) # RMA 판정 st.markdown("---") render_rma_section(result.rma_verdicts) # XID 에러 st.markdown("---") st.subheader("⚠️ XID 에러 분석") if not result.xid_events: st.success("XID 에러가 감지되지 않았습니다.") else: st.write(f"**총 {len(result.xid_events)}건의 XID 이벤트 감지**") summary_data = [] for code, info in result.xid_summary.items(): sev = XID_DB["severity_levels"].get(info["severity"], {}) summary_data.append({ "XID": code, "이름": info["name"], "횟수": info["count"], "심각도": f"{sev.get('emoji','')} {sev.get('label', info['severity'])}", "발생 GPU": ", ".join(info.get("gpus", [])) or "N/A", }) st.dataframe(pd.DataFrame(summary_data), use_container_width=True, hide_index=True) displayed = set() for event in result.xid_events: if event.xid_code in displayed: continue displayed.add(event.xid_code) sev = XID_DB["severity_levels"].get(event.severity, {}) gpu_label = f"GPU {event.gpu_index} ({event.gpu_name})" if event.gpu_name else event.pci_address serial_label = event.gpu_serial if event.gpu_serial else "N/A" with st.expander(f"{sev.get('emoji','ℹ️')} XID {event.xid_code}: {event.name} " f"({result.xid_summary[event.xid_code]['count']}건)", expanded=(event.severity == "critical")): st.markdown(f"**발생 GPU:** {gpu_label}") st.markdown(f"**시리얼 번호:** `{serial_label}`") st.markdown(f"**PCI 주소:** `{event.pci_address}`") st.markdown(f"**설명:** {event.description}") st.markdown("**권장 조치:**") for ln in event.action.split("\n"): st.markdown(f" {ln}") # 커널 이벤트 st.markdown("---") st.subheader("🐧 커널 로그 분석") if not result.kernel_events: st.success("주요 커널 에러가 감지되지 않았습니다.") else: kc1, kc2, kc3, kc4 = st.columns(4) kc1.metric("Critical", result.kernel_summary.get("critical", 0)) kc2.metric("Error", result.kernel_summary.get("error", 0)) kc3.metric("OOM", result.kernel_summary.get("oom", 0)) kc4.metric("PCIe", result.kernel_summary.get("pcie_error", 0)) event_data = [{"시각": e.timestamp, "유형": e.level.upper(), "키워드": e.message, "내용": e.raw_line[:200]} for e in result.kernel_events[:100]] st.dataframe(pd.DataFrame(event_data), use_container_width=True, hide_index=True) # 원본 로그 st.markdown("---") st.subheader("📄 원본 로그 검색") search_term = st.text_input("키워드 검색", placeholder="예: Xid, error, temperature...") log_text = read_log_file(file_content, uploaded_file.name) if search_term: matched = [f"**L{i+1}:** `{ln.strip()}`" for i, ln in enumerate(log_text.splitlines()) if search_term.lower() in ln.lower()] st.write(f"**{len(matched)}건 발견** (처음 50건)") for ln in matched[:50]: st.markdown(ln) else: st.text_area("로그 미리보기 (5000자)", log_text[:5000], height=200) # ============================================= # 페이지 2: 분석 이력 # ============================================= def page_history(): st.markdown('

📚 분석 이력

', unsafe_allow_html=True) st.markdown('

이전에 분석한 모든 로그 결과를 조회하고 관리합니다.

', unsafe_allow_html=True) # 필터 fc1, fc2 = st.columns([1, 3]) with fc1: status_filter = st.selectbox("상태 필터", ["전체", "critical", "warning", "healthy"]) with fc2: search = st.text_input("검색 (파일명, 메모, 드라이버 버전)", placeholder="키워드 입력...") records = get_analysis_list(limit=200, status_filter=status_filter, search=search or None) if not records: st.info("저장된 분석 이력이 없습니다. 로그 파일을 업로드하면 자동으로 저장됩니다.") return # 요약 통계 summary = get_status_summary() sc1, sc2, sc3, sc4 = st.columns(4) sc1.metric("전체 보고서", summary["total_reports"]) sc2.metric("🔴 심각", summary["by_status"].get("critical", 0)) sc3.metric("🟡 경고", summary["by_status"].get("warning", 0)) sc4.metric("🟢 정상", summary["by_status"].get("healthy", 0)) st.markdown("---") # 이력 테이블 status_emoji = {"critical": "🔴", "warning": "🟡", "healthy": "🟢"} table_data = [{ "ID": r["id"], "분석일시": r["created_at"], "상태": f"{status_emoji.get(r['overall_status'], '')} {r['overall_status']}", "파일명": r["filename"][:40], "GPU 수": r["gpu_count"], "XID": r["xid_count"], "심각": r["critical_count"], "경고": r["warning_count"], "드라이버": r["driver_version"], "메모": (r.get("memo") or "")[:50], } for r in records] st.dataframe(pd.DataFrame(table_data), use_container_width=True, hide_index=True) # 상세 조회 st.markdown("---") st.subheader("🔎 상세 조회") # 보고서 선택을 돕는 UI 개선: 드롭다운 선택박스 제공 history_options = [ (r["id"], f"[ID: {r['id']}] {status_emoji.get(r['overall_status'], '')} {r['filename'][:30]} ({r['created_at']})") for r in records ] selected_option = st.selectbox( "조회할 보고서 선택", options=history_options, format_func=lambda x: x[1] ) selected_id = selected_option[0] if selected_option else records[0]["id"] detail = get_analysis_detail(selected_id) if not detail: st.warning(f"ID {selected_id}에 해당하는 분석 결과가 없습니다.") return # 메모 편집 (editor/admin만) perms = get_perms() current_memo = detail.get("memo", "") or "" if perms["can_edit_memo"]: new_memo = st.text_area("📝 메모 수정", value=current_memo, key=f"memo_{selected_id}") if new_memo != current_memo: update_memo(selected_id, new_memo) st.success("메모가 업데이트되었습니다.") elif current_memo: st.text_area("📝 메모", value=current_memo, key=f"memo_{selected_id}", disabled=True) # 삭제 버튼 (admin만) if perms["can_delete"]: if st.button("🗑️ 이 분석 기록 삭제", type="secondary"): delete_analysis(selected_id) st.success(f"ID {selected_id} 삭제 완료. 페이지를 새로고침하세요.") return # 상세 내용 render_status_badge(detail["overall_status"]) dc1, dc2, dc3, dc4 = st.columns(4) dc1.metric("파일명", detail["filename"][:30]) dc2.metric("크기", f"{detail['file_size_mb']} MB") dc3.metric("드라이버", detail["driver_version"]) dc4.metric("CUDA", detail["cuda_version"]) # GPU 상세 st.markdown("---") st.subheader("🎮 GPU 상태") render_gpu_details_from_dict(detail["gpus"]) # RMA 판정 if detail.get("rma_verdicts"): st.markdown("---") render_rma_section(detail["rma_verdicts"], source="db") # XID 이벤트 if detail["xid_events"]: st.markdown("---") st.subheader(f"⚠️ XID 이벤트 ({len(detail['xid_events'])}건)") xid_df = pd.DataFrame([{ "XID": x["xid_code"], "이름": x["xid_name"], "심각도": x["severity"], "GPU": x.get("gpu_name") or "N/A", "S/N": x.get("gpu_serial") or "N/A", "PCI": x["pci_address"], "시각": x["timestamp"], } for x in detail["xid_events"]]) st.dataframe(xid_df, use_container_width=True, hide_index=True) # 커널 이벤트 if detail["kernel_events"]: st.markdown("---") st.subheader(f"🐧 커널 이벤트 ({len(detail['kernel_events'])}건)") ke_df = pd.DataFrame([{ "유형": k["level"].upper(), "키워드": k["message"], "시각": k["timestamp"], "내용": k["raw_line"][:150], } for k in detail["kernel_events"][:100]]) st.dataframe(ke_df, use_container_width=True, hide_index=True) # ============================================= # 페이지 3: XID 통계 # ============================================= def page_xid_stats(): st.markdown('

📊 XID 에러 통계

', unsafe_allow_html=True) st.markdown('

전체 분석 이력에서 XID 에러 코드별 발생 빈도와 트렌드를 확인합니다.

', unsafe_allow_html=True) stats = get_xid_statistics() if not stats: st.info("아직 XID 에러 데이터가 없습니다. 로그를 업로드하면 자동으로 축적됩니다.") return # 전체 통계 테이블 sev_emoji = {"critical": "🔴", "warning": "🟡", "info": "🟢"} stat_data = [{ "XID": s["xid_code"], "이름": s["xid_name"], "심각도": f"{sev_emoji.get(s['severity'], '')} {s['severity']}", "총 발생": s["total_count"], "관련 보고서": s["affected_reports"], "최초 발견": s["first_seen"], "최근 발견": s["last_seen"], } for s in stats] st.dataframe(pd.DataFrame(stat_data), use_container_width=True, hide_index=True) # XID 코드별 상세 조회 st.markdown("---") st.subheader("🔍 XID 코드 상세 조회") xid_codes = [s["xid_code"] for s in stats] if xid_codes: selected_xid = st.selectbox("XID 코드 선택", xid_codes, format_func=lambda x: f"XID {x}: {XID_DB['xid_errors'].get(str(x), {}).get('name', 'Unknown')}") xid_info = XID_DB["xid_errors"].get(str(selected_xid)) if xid_info: sev = XID_DB["severity_levels"].get(xid_info["severity"], {}) st.markdown(f"### {sev.get('emoji', '')} XID {selected_xid}: {xid_info['name']}") st.markdown(f"**설명:** {xid_info['description']}") st.markdown("**권장 조치:**") st.markdown(xid_info["action"]) # 전체 XID DB 조회 st.markdown("---") st.subheader("📖 XID 코드 사전") xid_lookup = st.number_input("XID 코드 입력", min_value=1, max_value=999, value=61, step=1) xid_info = XID_DB["xid_errors"].get(str(xid_lookup)) if xid_info: sev = XID_DB["severity_levels"].get(xid_info["severity"], {}) st.markdown(f"**{sev.get('emoji', '')} XID {xid_lookup}: {xid_info['name']}**") st.markdown(f"심각도: {sev.get('label', xid_info['severity'])}") st.markdown(xid_info['description']) st.markdown("**권장 조치:**") st.markdown(xid_info["action"]) else: st.warning(f"XID {xid_lookup}에 대한 정보가 데이터베이스에 없습니다.") # ============================================= # 페이지 4: GPU 시리얼 추적 # ============================================= def page_gpu_tracking(): st.markdown('

🔧 GPU 시리얼 추적

', unsafe_allow_html=True) st.markdown('

특정 GPU의 장애 이력을 시리얼 번호로 추적합니다. 동일 GPU의 반복 장애 패턴을 파악할 수 있습니다.

', unsafe_allow_html=True) serials = get_all_gpu_serials() if not serials: st.info("아직 GPU 시리얼 데이터가 없습니다. 로그를 업로드하면 자동으로 축적됩니다.") return # 전체 GPU 목록 st.subheader("📋 등록된 GPU 목록") gpu_table = [{ "시리얼 번호": s["serial_number"], "GPU 모델": s["name"], "UUID": s["gpu_uuid"], "분석 보고서 수": s["report_count"], } for s in serials] st.dataframe(pd.DataFrame(gpu_table), use_container_width=True, hide_index=True) # 시리얼 선택 st.markdown("---") st.subheader("🔎 시리얼 번호별 이력 조회") serial_options = [s["serial_number"] for s in serials] selected_serial = st.selectbox( "시리얼 번호 선택", serial_options, format_func=lambda x: f"{x} ({next((s['name'] for s in serials if s['serial_number']==x), 'N/A')})" ) if selected_serial: history = get_gpu_serial_history(selected_serial) if history: st.write(f"**{selected_serial}** 의 분석 이력: **{len(history)}건**") hist_data = [{ "ID": h["id"], "분석일시": h["created_at"], "파일명": h["filename"][:30], "상태": f"{'🔴' if h['overall_status']=='critical' else '🟡' if h['overall_status']=='warning' else '🟢'} {h['overall_status']}", "이슈": h["total_issues"], "심각": h["critical_count"], "온도": f"{h['temperature']}°C", "전력": f"{h['power_draw']}W", "메모리": f"{h['memory_used']}/{h['memory_total']} MiB", "메모": (h.get("memo") or "")[:40], } for h in history] st.dataframe(pd.DataFrame(hist_data), use_container_width=True, hide_index=True) # 온도 추이 차트 if len(history) > 1: st.markdown("**온도 추이**") chart_data = pd.DataFrame({ "분석일시": [h["created_at"] for h in history], "온도(°C)": [h["temperature"] for h in history], }) st.line_chart(chart_data.set_index("분석일시")) else: st.info("해당 시리얼의 이력이 없습니다.") # ============================================= # 메인 - 사이드바 네비게이션 # ============================================= def page_my_account(): """내 계정 정보 및 비밀번호 변경""" st.markdown('

🔐 내 계정

', unsafe_allow_html=True) user = get_current_user() if not user: return role_info = ROLES.get(user["role"], ROLES["viewer"]) st.write(f"**아이디:** `{user['username']}`") st.write(f"**이름:** {user['display_name']}") st.write(f"**역할:** {role_info['label']} ({user['role']})") st.write(f"**가입일:** {user['created_at']}") st.write(f"**최근 로그인:** {user.get('last_login') or '-'}") st.markdown("---") st.subheader("비밀번호 변경") with st.form("change_pw"): cur_pw = st.text_input("현재 비밀번호", type="password") new_pw = st.text_input("새 비밀번호 (6자 이상)", type="password") new_pw2 = st.text_input("새 비밀번호 확인", type="password") if st.form_submit_button("변경"): auth_result = authenticate(user["username"], cur_pw) if auth_result is None or "error" in auth_result: st.error("현재 비밀번호가 올바르지 않습니다.") elif new_pw != new_pw2: st.error("새 비밀번호가 일치하지 않습니다.") elif update_password(user["id"], new_pw): st.success("비밀번호가 변경되었습니다.") else: st.error("비밀번호는 6자 이상이어야 합니다.") def main(): # 로그인 안 되었으면 로그인 페이지만 표시 if not get_current_user(): page_login() return user = get_current_user() perms = get_perms() role_info = ROLES.get(user["role"], ROLES["viewer"]) with st.sidebar: st.markdown("### 🖥️ NV-Log Analyzer") st.markdown(f"**{user['display_name']}** ({role_info['label']})") # 메뉴 구성 (역할별) menu_items = [] if perms["can_upload"]: menu_items.append("🔍 로그 분석") menu_items.extend(["📚 분석 이력", "📊 XID 통계", "🔧 GPU 추적"]) if perms["can_manage_users"]: menu_items.append("👤 사용자 관리") menu_items.append("🔐 내 계정") page = st.radio("메뉴", menu_items, label_visibility="collapsed") st.markdown("---") if st.button("🚪 로그아웃", use_container_width=True): del st.session_state["user"] st.rerun() st.markdown( "NV-Log Analyzer v3.0
" "로그인 + 역할 기반 접근제어
" "nvidia-bug-report.log 전용
", unsafe_allow_html=True, ) if page == "🔍 로그 분석": page_analyze() elif page == "📚 분석 이력": page_history() elif page == "📊 XID 통계": page_xid_stats() elif page == "🔧 GPU 추적": page_gpu_tracking() elif page == "👤 사용자 관리": page_user_management() elif page == "🔐 내 계정": page_my_account() if __name__ == "__main__": main()