From e62f46b68c048a4bac3aee801b7424ef43bca627 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Mon, 27 Apr 2026 13:42:49 -0400 Subject: [PATCH] deeper log model --- app/__pycache__/config.cpython-314.pyc | Bin 6188 -> 6730 bytes app/config.py | 7 + app/routers/__pycache__/logs.cpython-314.pyc | Bin 2386 -> 3477 bytes app/routers/logs.py | 16 ++ app/services/__pycache__/ai.cpython-314.pyc | Bin 33371 -> 43514 bytes .../__pycache__/alertmanager.cpython-314.pyc | Bin 7408 -> 11742 bytes .../__pycache__/log_analyzer.cpython-314.pyc | Bin 18958 -> 20630 bytes .../__pycache__/log_ingest.cpython-314.pyc | Bin 24687 -> 37861 bytes app/services/__pycache__/pls.cpython-314.pyc | Bin 9342 -> 10238 bytes app/services/ai.py | 143 ++++++++++- app/services/alertmanager.py | 67 ++++- app/services/log_analyzer.py | 33 +++ app/services/log_ingest.py | 235 +++++++++++++++++- app/services/pls.py | 10 + app/ui/index.html | 194 ++++++++++++--- config/marvis.env.example | 4 + config/p5g-marvis.service | 8 + 17 files changed, 670 insertions(+), 47 deletions(-) diff --git a/app/__pycache__/config.cpython-314.pyc b/app/__pycache__/config.cpython-314.pyc index 3a854e45953c4e4e1bfc8f015e040096bb8985ce..e51ccbfb9ec9e5d2bb6d05dc3ec0e2694f9ada5c 100644 GIT binary patch delta 1173 zcmbV}O-~a+7{_P3t-NT9#kTZKC=_T5ls6Hf2&FAcTl&hgZ2^@QCJkmM%D_RoMeBQ{Xf5%*?s0k@4Ep-r$Szf$k@35 zHM?8CqtG&6URmT4H?$5&XpT@pH7{vK&{mPC%Sn4uCOVpOVyQ?PcE0KQtY8xrZCUB4 z*hW`QI-yH+^kh-39lC!9xQ;b8^gypzH_o%W# z5QOn_QiwoQELccEK3%bul?DL3XxL~FgSZ%U&>#UbV$ey0S(rNsqVx|9RoP*B5nZIX zeFJWu6bp9fhatEE!*CTGa0^CY9ZE7c7DsmcRpPScVL&^e775 zq(65eOrZxDGwEAXxp&!$neFZ;di~)vLr5Pw&f%>pU-6@Cw%Qqmh==3(lsi5-$w4aU z4|9C>n2174$OQ>Mp9)62DL(GzJ>Yi}qf!)_@VG*UhZH;bgJNRASg|4oE)SQQ;M{R< zD##_cpdee$EN!j|rZZB^1zs+e;yH30`4)j)Tw4*?JjL2tMqoGBR|IK(^LZbk^IM)_=8CF#O$0Ja_5Ypqgf9nw8U#y#(E#+NmBrP;D`3ysa9=uT`1zkBLmL zHVm)|$>C1k)pO_Dn>wLGnPR8#8k?OFCsP(tcMhvz`r zE=aA%GIm7r1F_BQ5hAmftxxq2^-qlD7lLNCgmg#M$l+m%yP7;>^fM&;`MsJovH1(+ R^fzPLfL;2wT^s0^{sK5x6<`1W delta 700 zcmZ{g&rVZ87{zDqv|z;kri>b;E#6BAvyG9kW!-McbrT=)RYko3yNN#C+y#273qmkF+%A^!HC;Or=3mPI|ajA7g&L#HH;35x_o(|iJx z76_J^$el_OJT1~5b(1a9Qj%ilyo9_Ur3Jp*KgHvJY$QpJnr%XThvj&hSeXF zftx7yK&dF+JJMfG%gK$4Vx@$8y`m8Zki>mV;{j%{g;_kr9JVo!M@V4@3)sa~JjONb zVG(7dv5zM>KxNu#Oo%}p7O9Z{jf(Mv#cq;1iPy~GzE9_P+U4XiSKD6yO9|nhU0HL_ zolxc~jiV0JrL>w-4jWRm4tuU0b!?TEE?~1Nq|vB#Z2Gg8RsPN!_ItEd@c}I>Wlc&> z2><2{UeF5ix*=tg`+OPSe~g~9M&XW6ny-CRly?W;@Gt&XbcZjhOLk3_xh=Ie@Kzke l%Zc7q%wKAV@^E0A{Y3&Bh+XU`!WHs^2rva( zKHfdv(v;0(woMC7LS&R{D9s1%WhpJ3Cs}ZxN&^7yCrPWrHWhA^W9fvPgE`J17EFK% zCK>1V6BCAc6KD#9x=1BdUev#C!LH5Nv(%g^(SlEdrZGqb2J8WJjt()qAHinr55#E* zl^1H2J41{+9YGgsZl6ThKxUf)FHXTnHMXvftf?c*PD9PDhjXie+`DkjeJl4*DsX*Y zlH7xYaGy_-hmcp`5hTe{C?DcqXnBcWkwi3CLOezmyZ@MU)pE-r8xs_j&*{}G~x z*)`dC+eCtXRg3!=x-%%$hofleVNbqPM6Q-ec^2^}BckQ)Dq8MjU~rKA41WNWmD3Ie z>w+spWz9iQ(`5 ZmuT{Y>-#G&-hkdMznH}n-+a?Q@fSgcwb%dv delta 112 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 delta 6091 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 diff --git a/app/services/__pycache__/alertmanager.cpython-314.pyc b/app/services/__pycache__/alertmanager.cpython-314.pyc index 67eb625403362bbdf4c2294840c235815296fd03..437ed991b3a6cd28fc2cbefa5e9ea82897444e6f 100644 GIT binary patch delta 4258 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 Fe*knpUqb)@ delta 458 zcmcZ?{lSt?n~#@^0SE-wyv|(9Kao#@an(fiRjiE444Q%)f1P3DjA3SAP-2j0Xqfy_ zOnLGr_NS~b7#IpzCvl2T{>*WaCxlBKq#X!S#3w(JR}qQiGGT%U_%iA-6mW;I1c*+4 z$S*wkD`$`ND+UHm204Zh7G(x@23JNOUofe~&fxQB@*FN>Mv=`IxcC_vxhFs1_TVc5 z+RSi^H!U+SB|a~0vJp=?WA)@!Jl90-yZT%aP`oane@Q@p1N#*L#|uFrlY4mQ)ax*| zD7#5AEN65z;aI^8WUW?mwP8KT%jjy(aga|1#5Upv3Lms(bhTnRXvYF%A7XMga?@cv zYQhNQ95rVIDmrQfX4|m2iE$odVsvBWJI2ffV)HS9NCA+xVgwj7h>b<<=% zrX~Ou(qIH}G?{_YlQT7JH-F)4<6zX>TrGZ>nUQC*y|f*p=H!{uPK?%*??|gC-WJa6 zV86jF)MtM~SgOnKBP)YQ7UM@Y1|Gg5UZD3i1t&ky$zfbMIbK(cs|P5+2*kzilk0U| MxxRC-GD1`V07%ny!~g&Q diff --git a/app/services/__pycache__/log_analyzer.cpython-314.pyc b/app/services/__pycache__/log_analyzer.cpython-314.pyc index aa8de51e0aa518147093de417439078e92146c48..29b72b2005b90e4dd6363298f91c99565b110aa4 100644 GIT binary patch delta 2490 zcmZuzeQZePKT?Cgdfy8q z&@C2fS2i@1^rlPJ3Xy7b1GN+F(rMk7HUz8IwUd$p!@Ol0+dmbnw9&Dy+aJ?*&a(l- zxUzrm+;e{Cocqqb=e>7gmVWDHTG3ju%1mH{j{hxrAv{yzsMt#hiIF^sY_t=(aVB(< z24~(Mr6InN4pcpCCy@@ zjuh z>q_z%Xk8xS0k;T&deCs`PAf?--3F|jHmc>*p4xg%L>nsEa=H;i$)BiJuQ^o8xXZ+! z(mQoIq3Goi%DnU=mfK#pm&K`I<4Ydn=1Q{dCxje=1Z*N|gxNVrZ7NG+is;Hqe{Vi- z&OH3erbC1z@OH>RD8A7iMd$AOO)v4)4QOg>C^rQ#>Ph%ZwE+LR0sF9I?ptG#t!kAIlEl2SfZiFas;4+f|4Lboco93}bpYU@K`QmepIZD6D zPucwR^kSpOLE&~gy#f1qB&w$v-2=L!=Xrk6yIy!3G^hD@ycg)X#WA0++Q+iUehP_{ zJqwqzZe=pe`A>sh`W{~iy7sbz$lZhR1j6SLCJM-V0vo(51%`%>f31ACKlz^tD3 zGX8y3XuWY37umD?Q0QT|+8IJTI}QzW2_u=~*^Des$fL;w`z`-f=;$ik$Fmw1#AFkE zcSncDN%`@PAXMNN9a&gzd*`m+KcH3J@jrqj;rn8(SM4FziKgoSt-_#isVkcVNnXgYYiGdwg)y(|$IEvL8S(iJ9@V$iV;Z1Bm6p!vDT0 z>S9NcquZZNW*B}FFx**&$AG=f$GUn1mI9f-+U5TLMAy1TD{mqbpH%7=vk&;dW?u{Y z8Y-Scz>>3L2wz9|2Et{8D+t&4YnvaVC%Dk%cW0I} zvq@80mG(_=F&xnvtVATxHZ4+?TImn8h^SRCX`j4;eegx`pCA|%^_)!wsSY#WoH^(F zzB~7v``X*0`l<*$7-|hDbRAv(&MFShhr*#Jg`ybBi^`sXPQ~k<-*!SgJbxo6260e~ ztD;ZQ(zU8sX~UP3qnPj7CZKk_Yr-dNT#U8jAH50TVLUeH2Qmehk{xP=!Wq02i;5YP zv3+-z4&r2-OLqLxvgXAR}4SP;&fFk?aIFyL1Z*o|BI#CwlIs7b{ z5XWl2B*TKX8%SxLi(Fk`V7cboP%h#VseW~dGRJT>^?|6?hWq1fX{|x3X4%O<3sEME zx=<68oH1?BrM4^gLyAx~j}(p4{(_<7ksUFyjBoAe78<@Ajz81v5*aLSP6i96n+L1x zS&l7(V3y2+Wz;`f4iqb7xZm=$fNahio;l^f9PB1%IM47N!v%)-38de3Dv&qf9Ci;6 zL@#nm%{vv_gG(F-@{aA92fdtCfHOEfe6*EEd#VA_^n&xaK0MU0uHe8(nwnA=ap-?< z#Ul8_$fLmznZa!M2)B;5CqJezK>LJGICq`kkl!ep;BApjvj*6fJq2a7M)!!5czJZX z=_)ZM0C0=mtFpEqck4akIy!nsP_}qh$#gxVRK}O|&Ehnk)wkY#l2~wx;WWcCey<3S%3uPWQ%E6N+wWSU;!hM7@in=yjS{-a@n*C(l_l`HmEG-kwSg`OZexQ?w7uz z<$9RSME|>tlgZut1-ttrRU6$2s%QaV|N9O3~Dzn<{iG?$@|9 LxhgvF$garWQ}11~ diff --git a/app/services/__pycache__/log_ingest.cpython-314.pyc b/app/services/__pycache__/log_ingest.cpython-314.pyc index ce39e8a79005de346c3c44572cfccb42b0ab0a9e..acf8f657cfa0389b8552f0992c00acd4af94dd58 100644 GIT binary patch delta 17277 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 LZ&vww&BFfzl~Tln diff --git a/app/services/__pycache__/pls.cpython-314.pyc b/app/services/__pycache__/pls.cpython-314.pyc index 58b8eed8f23e74bf1a17be938700e28609c38195..9ddcc90474f7bcf6d82c3ebf152cca2eb2d978d8 100644 GIT binary patch delta 711 zcmZ9JO=uHQ5XX0RF{McpyEWftH`#1WTs)LYidLIz@n9o7P@#%VDJD&unD}-ZY*mPe zAf60^;?ZM+3QEoe#k+VYRK$x%PuhbNFU5J=ZT)!gel!31;LYsrp4+_;IuQ!`B|1+Z zeXnh(-B28I-F$o8V1qG<`D0RjxDL(;%d!CJAx|G+1`Co7c{=zj{ZHBhfJMBPjzJCEX&qYl zBwd7Me3hK+4`l)XPCsr`AYs$^J%wK+!{-HN2$sy-_(qH1%WM=@@qM<0jno;LSFm|# zRKHCXc9PJMb|e`J+XBzlehBmW3WD~y+*8(u8`Xw0w$Qv$sa417&8p28sinlrL|!A< zG9j1Y5i*}c)$7h2Rn6@+@VgXr-x(+88@=>JjeLwAUw1au!d9SwU$ff4#4o-~S-kI` z_O;33Gx#EPy4WGtHVA)@_dM*5w?6Bdw`bj%d6=pe#2EX>hY3yhT?PiH6fA17pnO%? z&s~ynUNfq)DKWYO(s_MCsR6%3d47vvDcqeRE&`JTTeRj5Hf*i@vtfsl;@bahnA=pR zwgOXP!^6eB4J{~HFm36WFy9$V)Wcf>j=-vb_*%R}uz=sgZ8PD2NCN2EH!(a~PW<%K Lzu7s!pY4AGY}KUm delta 292 zcmez8|IdR@n~#@^0SG$Qzs~d(n8+u=$Tm^E%uqInQ=B1)OPnD`AcwI;C`c}d8_4H@ z@`ZzBgLr{_J}6%#NOs~mbwSx6QJ|n0R8S&Fb}}2IIxDj>gQooEAjVSZ$@Ow#oA1g< zF*0Uu{vq4S$e2F4PF|cbWAZe4ImZ0STjgyTvnHRFvfQkrz|F{5yxBrAjZv})=r~R0 zqEH}Jln5exK>|!grIRnpicUVGB*s`i`LU88hbE(+CZn6C%;W~OgI1X!h4~<&7(|qV yv@jM`0I6a|AmLCX3M6lF*yQG?l;)(`71aZ|Ac^9n$*t-MT;I8v7)^>qfC2z7v_rrE diff --git a/app/services/ai.py b/app/services/ai.py index ce4d3da..b91c9f7 100644 --- a/app/services/ai.py +++ b/app/services/ai.py @@ -9,6 +9,7 @@ from datetime import datetime import re from app.config import ( AI_MODE, + ALL_NFS, CONTAINER_RUNTIME, OPENAI_API_KEY, OPENAI_MODEL, @@ -19,6 +20,9 @@ from app.config import ( async def answer(query: str, network_state: dict, alerts: list, logs: list[dict] | None = None) -> str: + special = await _handle_log_queries(query, network_state, alerts, logs or []) + if special: + return special if AI_MODE == "openai": return await _call_openai(query, network_state, alerts, logs or []) if AI_MODE == "ollama": @@ -53,7 +57,6 @@ def _rule_based(query: str, network_state: dict, alerts: list, logs: list[dict]) ) # Specific NF query - from app.config import ALL_NFS for nf_name in ALL_NFS: if nf_name.lower() in q: return _nf_detail(nf_name, nfs, alerts, log_hits) @@ -74,6 +77,60 @@ def _rule_based(query: str, network_state: dict, alerts: list, logs: list[dict]) return _health_summary(up, down, alerts, cluster, log_hits) +async def _handle_log_queries(query: str, network_state: dict, alerts: list, logs: list[dict]) -> str | None: + from app.services import log_analyzer, log_ingest + + q = query.strip() + lowered = q.lower() + + if "trace" in lowered and any(word in lowered for word in ["stop", "end", "disable", "finish"]): + summary = await log_ingest.stop_subscriber_trace() + if not summary.get("started_at"): + return "ℹ️ No subscriber trace is currently active." + return ( + f"πŸ›‘ **Subscriber trace stopped** for `{summary.get('filter')}`\n\n" + f"Started: {summary.get('started_at')}\n" + f"Matched events: **{summary.get('matched_events', 0)}**\n" + f"Restored nodes: {', '.join(summary.get('restored_nodes', [])) or 'none'}" + ) + + trace_target = _extract_trace_target(q) + if trace_target: + state = await log_ingest.start_subscriber_trace(trace_target) + events = log_ingest.get_subscriber_events(trace_target, limit=20) + findings = log_analyzer.summarize_event_slice(events) + return _format_trace_response(trace_target, state, events, findings) + + supi_query = _extract_supi_query(q) + asks_logs = any( + phrase in lowered + for phrase in ["show me the logs", "show logs", "logs for", "what do the logs show", "trace output", "recent logs"] + ) + nf_query = _extract_nf_query(q) + + if supi_query and (_is_bare_supi(q) or "subscriber" in lowered or "supi" in lowered or "imsi" in lowered or asks_logs): + events = log_ingest.get_subscriber_events(supi_query, limit=500) + findings = log_analyzer.summarize_event_slice(events) + return _format_log_slice( + title=f"Subscriber logs for `{supi_query}`", + events=events, + findings=findings, + empty_message=f"ℹ️ No recent logs matched subscriber `{supi_query}`.", + ) + + if nf_query and ("process" in lowered or asks_logs or "show me" in lowered): + events = log_ingest.get_process_events(nf_query, limit=500) + findings = log_analyzer.summarize_event_slice(events) + return _format_log_slice( + title=f"Process logs for `{nf_query}`", + events=events, + findings=findings, + empty_message=f"ℹ️ No recent logs are buffered for process `{nf_query}`.", + ) + + return None + + def _health_summary(up: list, down: list, alerts: list, cluster: dict, log_hits: list[dict]) -> str: ts = datetime.now().strftime("%H:%M:%S") crit = [a for a in alerts if a.get("severity") == "critical"] @@ -237,6 +294,90 @@ def _log_summary(log_hits: list[dict], logs: list[dict]) -> str: return "\n".join(lines) +def _extract_supi_query(query: str) -> str: + lowered = query.lower() + match = re.search(r"(imsi-\d{6,20}|\b\d{6,20}\b)", lowered) + if not match: + return "" + token = match.group(1) + if token.startswith("imsi-"): + return token + return f"imsi-{token}" + + +def _is_bare_supi(query: str) -> bool: + cleaned = query.strip().lower() + return bool(re.fullmatch(r"(imsi-\d{6,20}|\d{6,20})", cleaned)) + + +def _extract_nf_query(query: str) -> str: + text = query.upper() + for nf_name in ALL_NFS: + if nf_name in text: + return nf_name + return "" + + +def _extract_trace_target(query: str) -> str: + lowered = query.lower() + if "trace" not in lowered: + return "" + if not any(word in lowered for word in ["start", "run", "begin", "trace"]): + return "" + return _extract_supi_query(query) + + +def _format_log_slice(*, title: str, events: list[dict], findings: list[dict], empty_message: str) -> str: + if not events: + return empty_message + lines = [f"🧾 **{title}**", f"Buffered lines: **{len(events)}**\n"] + if findings: + lines.append("Rule hits:") + for finding in findings[:6]: + lines.append( + f"β€’ **{finding['severity'].upper()}** {finding['nf']} on {finding.get('node','unknown')}: " + f"{finding['description']}" + ) + lines.append(f" Fix: {finding['remediation']}") + lines.append("") + lines.append("Recent log lines:") + for event in events[-12:]: + lines.append( + f"β€’ {event.get('timestamp','')} β€” {event.get('node','unknown')} {event.get('nf','SYSTEM')}: " + f"{_trim_message(event.get('message',''), 220)}" + ) + return "\n".join(lines) + + +def _format_trace_response(target: str, state: dict, events: list[dict], findings: list[dict]) -> str: + lines = [ + f"πŸ”Ž **Subscriber trace active** for `{target}`", + f"Level override: **{state.get('level', 'debug')}**", + f"Nodes updated: {', '.join(state.get('nodes', [])) or 'none'}", + f"Matched events so far: **{state.get('matched_events', 0)}**\n", + ] + if findings: + lines.append("Current rule-based diagnosis:") + for finding in findings[:5]: + lines.append( + f"β€’ **{finding['severity'].upper()}** {finding['nf']} on {finding.get('node','unknown')}: " + f"{finding['description']}" + ) + lines.append(f" Fix: {finding['remediation']}") + lines.append("") + if events: + lines.append("Current trace lines:") + for event in events[-10:]: + lines.append( + f"β€’ {event.get('timestamp','')} β€” {event.get('node','unknown')} {event.get('nf','SYSTEM')}: " + f"{_trim_message(event.get('message',''), 220)}" + ) + else: + lines.append("No matching subscriber logs have arrived yet.") + lines.append("\nUse `stop trace` when the attach/session test is complete.") + return "\n".join(lines) + + def _nf_label(nf: dict) -> str: placements = nf.get("nodes", []) if not placements: diff --git a/app/services/alertmanager.py b/app/services/alertmanager.py index bb2aa0e..1d7f12f 100644 --- a/app/services/alertmanager.py +++ b/app/services/alertmanager.py @@ -41,7 +41,7 @@ async def _get_alertmanager_alerts(cluster: dict) -> list: name = labels.get("alertname", "Unknown") summary = annotations.get("summary", annotations.get("description", "")) nf_name = _infer_nf(name, summary, labels.get("instance", "")) - nodes = cluster_inventory.find_nf_nodes(cluster, nf_name) if nf_name else [] + nodes = _resolve_nodes(cluster, labels, name, summary, nf_name) alerts.append({ "name": name, "severity": labels.get("severity", "warning"), @@ -107,7 +107,70 @@ def _severity_rank(severity: str | None) -> int: def _infer_nf(name: str, summary: str, instance: str) -> str: text = f"{name} {summary} {instance}".upper() - for nf_name in ["AMF", "SMF", "UPF", "UDM", "UDR", "NRF", "AUSF", "PCF", "MME", "SGWC", "DRA", "DSM"]: + for nf_name in ["AMF", "SMF", "UPF", "UDM", "UDR", "NRF", "AUSF", "PCF", "MME", "SGWC", "DRA", "DSM", "AAA", "BMSC", "CHF", "SMSF", "EIR"]: if nf_name in text: return nf_name return "" + + +def _resolve_nodes(cluster: dict, labels: dict, name: str, summary: str, nf_name: str) -> list: + nodes = cluster.get("nodes", []) + if not nodes: + return [] + + search_parts = [name, summary] + for key in ("instance", "job", "service", "container", "hostname", "node", "pod"): + value = labels.get(key) + if value: + search_parts.append(value) + search_text = " ".join(search_parts).upper() + + explicit = [] + for node in nodes: + hostname = str(node.get("hostname", "")) + address = str(node.get("address", "")) + node_name = str(node.get("name", "")) + if any(token and token.upper() in search_text for token in (hostname, address, node_name)): + explicit.append(_node_ref(node)) + if explicit: + return _dedupe_nodes(explicit) + + if nf_name: + nf_nodes = cluster_inventory.find_nf_nodes(cluster, nf_name) + if nf_nodes: + return nf_nodes + + service_matches = [] + for node in nodes: + started = {str(service).upper() for service in node.get("started_services", [])} + if any(service and service in search_text for service in started): + service_matches.append(_node_ref(node)) + if service_matches: + return _dedupe_nodes(service_matches) + + current = next((node for node in nodes if node.get("current")), None) + if current and labels.get("instance", "").startswith(("127.0.0.1", "localhost")): + return [_node_ref(current)] + + return [] + + +def _node_ref(node: dict) -> dict: + return { + "hostname": node.get("hostname", ""), + "address": node.get("address", ""), + "role": node.get("role", "AP"), + "current": node.get("current", False), + } + + +def _dedupe_nodes(nodes: list[dict]) -> list[dict]: + seen = set() + result = [] + for node in nodes: + key = (node.get("hostname"), node.get("address")) + if key in seen: + continue + seen.add(key) + result.append(node) + return result diff --git a/app/services/log_analyzer.py b/app/services/log_analyzer.py index 4920e2b..9b513e6 100644 --- a/app/services/log_analyzer.py +++ b/app/services/log_analyzer.py @@ -109,6 +109,39 @@ def _rule_matches(message: str, pattern: str) -> bool: return False +def summarize_event_slice(events: list[dict]) -> list[dict]: + findings: list[dict] = [] + seen: set[tuple[str, str, str, str]] = set() + for event in sorted(events or [], key=lambda item: item.get("epoch", 0.0)): + message = event.get("message", "") + event_nf = str(event.get("nf", "")).upper() + event_node = event.get("node", "") + for category, patterns in load_category_patterns().items(): + for rule in patterns: + rule_nf = str(rule["nf"]).upper() + if rule_nf != event_nf: + continue + if not _rule_matches(message, rule["pattern"]): + continue + key = (category, rule_nf, event_node, rule["description"]) + if key in seen: + continue + seen.add(key) + findings.append( + { + "category": category, + "nf": rule_nf, + "node": event_node, + "severity": rule["severity"], + "description": rule["description"], + "remediation": rule["remediation"], + "message": message, + "timestamp": event.get("timestamp", ""), + } + ) + return findings + + # ── Category/NF mapping for Alertmanager alerts ────────────────────────────── def _alert_category(alert: dict) -> str: diff --git a/app/services/log_ingest.py b/app/services/log_ingest.py index b925bff..931a6c6 100644 --- a/app/services/log_ingest.py +++ b/app/services/log_ingest.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import json +import re import sqlite3 from collections import deque from datetime import UTC, datetime @@ -22,22 +23,43 @@ from app.config import ( LOG_AUTO_CONFIGURE, LOG_FLUENTBIT_MATCH, LOG_INGEST_ENABLED, + LOG_PROCESS_BUFFER_LINES, LOG_RECEIVER_BIND_HOST, LOG_RECEIVER_FORMAT, LOG_RECEIVER_HOST, LOG_RECEIVER_PORT, + LOG_SUBSCRIBER_BUFFER_LINES, + LOG_TRACE_DEBUG_LEVEL, LOG_TRACE_BUFFER_LINES, + LOG_TRACE_TARGET_SERVICES, ) from app.services import pls _server: asyncio.base_events.Server | None = None +_allowed_nfs = {nf.upper() for nf in LOG_ALLOWED_NFS} _events: deque[dict[str, Any]] = deque(maxlen=max(LOG_BUFFER_LINES, 1)) _trace_events: deque[dict[str, Any]] = deque(maxlen=max(LOG_TRACE_BUFFER_LINES, LOG_BUFFER_LINES, 1)) +_process_events: dict[str, deque[dict[str, Any]]] = { + nf.upper(): deque(maxlen=max(LOG_PROCESS_BUFFER_LINES, 1)) + for nf in _allowed_nfs if nf != "SYSTEM" +} +_subscriber_events: dict[str, deque[dict[str, Any]]] = {} _ingested_total = 0 _parse_errors = 0 _last_event_at: str | None = None _db_initialized = False -_allowed_nfs = {nf.upper() for nf in LOG_ALLOWED_NFS} +_supi_pattern = re.compile(r"(imsi-\d{6,20}|\b\d{6,20}\b)", re.IGNORECASE) +_trace_state: dict[str, Any] = { + "active": False, + "filter": "", + "normalized": "", + "started_at": None, + "matched_events": 0, + "nodes": [], + "services": list(LOG_TRACE_TARGET_SERVICES), + "level": LOG_TRACE_DEBUG_LEVEL, + "original_levels": {}, +} def _db_path() -> Path: @@ -181,6 +203,43 @@ def _infer_nf(payload: dict[str, Any], message: str) -> str: return "SYSTEM" +def _normalize_supi(value: str | None) -> str: + if not value: + return "" + text = str(value).strip().lower() + if not text: + return "" + if text.startswith("imsi-"): + digits = "".join(ch for ch in text[5:] if ch.isdigit()) + return f"imsi-{digits}" if digits else text + digits = "".join(ch for ch in text if ch.isdigit()) + if digits: + return f"imsi-{digits}" + return text + + +def _extract_supis(message: str) -> list[str]: + matches = [] + for raw in _supi_pattern.findall(message or ""): + normalized = _normalize_supi(raw) + if normalized and normalized not in matches: + matches.append(normalized) + return matches + + +def _matches_trace(event: dict[str, Any]) -> bool: + if not _trace_state.get("active"): + return False + normalized = _trace_state.get("normalized", "") + if not normalized: + return False + message = str(event.get("message", "")).lower() + if normalized in message: + return True + digits = normalized.removeprefix("imsi-") + return bool(digits and digits in message) + + def _normalize_event(payload: dict[str, Any], remote_host: str) -> dict[str, Any]: ts_value = ( payload.get("timestamp") @@ -215,6 +274,7 @@ def _normalize_event(payload: dict[str, Any], remote_host: str) -> dict[str, Any or "" ) message = str(message).strip() + supis = _extract_supis(message) tag = str(payload.get("tag", "")) nf = _infer_nf(payload, message) fingerprint = sha1(f"{ts_iso}|{node}|{nf}|{source}|{message}".encode("utf-8")).hexdigest() @@ -227,6 +287,7 @@ def _normalize_event(payload: dict[str, Any], remote_host: str) -> dict[str, Any "source": str(source), "tag": tag, "message": message, + "supis": supis, "raw": payload, } @@ -238,6 +299,16 @@ async def _ingest_payload(payload: dict[str, Any], remote_host: str) -> None: return _events.append(event) _trace_events.append(event) + nf_key = event.get("nf", "").upper() + if nf_key: + _process_events.setdefault(nf_key, deque(maxlen=max(LOG_PROCESS_BUFFER_LINES, 1))).append(event) + for supi in event.get("supis", []): + _subscriber_events.setdefault( + supi, + deque(maxlen=max(LOG_SUBSCRIBER_BUFFER_LINES, 1)), + ).append(event) + if _matches_trace(event): + _trace_state["matched_events"] = int(_trace_state.get("matched_events", 0)) + 1 _ingested_total += 1 _last_event_at = event["timestamp"] @@ -300,6 +371,8 @@ def receiver_status() -> dict[str, Any]: "format": LOG_RECEIVER_FORMAT, "allowed_nfs": sorted(_allowed_nfs), "buffer_lines": LOG_BUFFER_LINES, + "process_buffer_lines": LOG_PROCESS_BUFFER_LINES, + "subscriber_buffer_lines": LOG_SUBSCRIBER_BUFFER_LINES, "trace_buffer_lines": LOG_TRACE_BUFFER_LINES, "context_before": LOG_ALERT_CONTEXT_BEFORE, "context_after": LOG_ALERT_CONTEXT_AFTER, @@ -308,6 +381,17 @@ def receiver_status() -> dict[str, Any]: "parse_errors": _parse_errors, "last_event_at": _last_event_at, "current_buffer_size": len(_events), + "process_buffers": sorted(_process_events.keys()), + "subscriber_buffers": len(_subscriber_events), + "trace": { + "active": bool(_trace_state.get("active")), + "filter": _trace_state.get("filter", ""), + "started_at": _trace_state.get("started_at"), + "matched_events": _trace_state.get("matched_events", 0), + "nodes": list(_trace_state.get("nodes", [])), + "services": list(_trace_state.get("services", [])), + "level": _trace_state.get("level", LOG_TRACE_DEBUG_LEVEL), + }, } @@ -396,8 +480,135 @@ async def configure_site_output() -> dict[str, Any]: } +def _sort_and_limit(events: list[dict[str, Any]], limit: int | None = None) -> list[dict[str, Any]]: + deduped: dict[str, dict[str, Any]] = {} + for event in events: + deduped[event.get("id", str(id(event)))] = event + ordered = sorted(deduped.values(), key=lambda event: event.get("epoch", 0.0)) + if limit is not None: + return ordered[-limit:] + return ordered + + +def get_process_events(nf: str, limit: int | None = None) -> list[dict[str, Any]]: + nf_key = str(nf or "").upper() + events = list(_process_events.get(nf_key, [])) + return _sort_and_limit(events, limit) + + +def get_subscriber_events(supi_or_fragment: str, limit: int | None = None) -> list[dict[str, Any]]: + normalized = _normalize_supi(supi_or_fragment) + fragment = str(supi_or_fragment or "").strip().lower() + if not normalized and not fragment: + return [] + matches: list[dict[str, Any]] = [] + for supi, events in _subscriber_events.items(): + digits = supi.removeprefix("imsi-") + if normalized and (supi == normalized or normalized in supi or normalized.removeprefix("imsi-") in digits): + matches.extend(events) + continue + if fragment and (fragment in supi.lower() or fragment in digits): + matches.extend(events) + return _sort_and_limit(matches, limit) + + +async def _trace_target_nodes() -> list[dict[str, Any]]: + cluster = await pls.get_cluster_status() + nodes = [] + if isinstance(cluster, dict): + for node in cluster.get("nodes", []): + host = pls.node_host(node.get("name", "")) + if host: + nodes.append({"name": node.get("name", ""), "host": host}) + if not nodes: + system = await pls.get_system_info() + host = str(system.get("hostname", "") if isinstance(system, dict) else "") or "127.0.0.1" + nodes.append({"name": host, "host": host}) + deduped = {} + for node in nodes: + deduped[node["host"]] = node + return list(deduped.values()) + + +async def start_subscriber_trace(supi_or_fragment: str) -> dict[str, Any]: + normalized = _normalize_supi(supi_or_fragment) + fragment = str(supi_or_fragment or "").strip() + if not normalized and not fragment: + raise RuntimeError("A SUPI or SUPI fragment is required to start a trace") + + if _trace_state.get("active"): + await stop_subscriber_trace() + + target_nodes = await _trace_target_nodes() + original_levels: dict[str, dict[str, Any]] = {} + applied_nodes: list[str] = [] + for node in target_nodes: + host = node["host"] + current = await pls.get_log_config(host=host) + if not isinstance(current, dict): + continue + original_levels[host] = current + updated = dict(current) + updated["level"] = LOG_TRACE_DEBUG_LEVEL + await pls.put_log_config(updated, host=host) + applied_nodes.append(host) + + _trace_state.update( + { + "active": True, + "filter": fragment, + "normalized": normalized or fragment.lower(), + "started_at": datetime.now(UTC).isoformat(), + "matched_events": 0, + "nodes": applied_nodes, + "services": list(LOG_TRACE_TARGET_SERVICES), + "level": LOG_TRACE_DEBUG_LEVEL, + "original_levels": original_levels, + } + ) + return receiver_status()["trace"] + + +async def stop_subscriber_trace() -> dict[str, Any]: + original_levels = dict(_trace_state.get("original_levels", {})) + restored_nodes: list[str] = [] + for host, config in original_levels.items(): + try: + if isinstance(config, dict): + await pls.put_log_config(config, host=host) + restored_nodes.append(host) + except Exception: + continue + + summary = { + "filter": _trace_state.get("filter", ""), + "started_at": _trace_state.get("started_at"), + "matched_events": _trace_state.get("matched_events", 0), + "restored_nodes": restored_nodes, + } + _trace_state.update( + { + "active": False, + "filter": "", + "normalized": "", + "started_at": None, + "matched_events": 0, + "nodes": [], + "services": list(LOG_TRACE_TARGET_SERVICES), + "level": LOG_TRACE_DEBUG_LEVEL, + "original_levels": {}, + } + ) + return summary + + def get_events(limit: int | None = None, node: str | None = None, nf: str | None = None, imsi: str | None = None) -> list[dict[str, Any]]: - events = list(_trace_events if imsi else _events) + if imsi: + events = get_subscriber_events(imsi, limit=None) + elif nf: + events = get_process_events(nf, limit=None) + else: + events = list(_events) if node: node_l = node.lower() events = [event for event in events if event.get("node", "").lower() == node_l] @@ -405,12 +616,20 @@ def get_events(limit: int | None = None, node: str | None = None, nf: str | None nf_u = nf.upper() events = [event for event in events if event.get("nf", "").upper() == nf_u] if imsi: - needle = imsi.strip() - events = [event for event in events if needle and needle in event.get("message", "")] - events.sort(key=lambda event: event.get("epoch", 0.0)) - if limit is not None: - return events[-limit:] - return events + needle = str(imsi).strip().lower() + normalized = _normalize_supi(imsi) + digits = normalized.removeprefix("imsi-") if normalized else "" + events = [ + event for event in events + if needle and ( + needle in event.get("message", "").lower() + or any( + needle in supi.lower() or (digits and digits in supi.removeprefix("imsi-")) + for supi in event.get("supis", []) + ) + ) + ] + return _sort_and_limit(events, limit) def record_alert_context( diff --git a/app/services/pls.py b/app/services/pls.py index 5005af8..42e27d2 100644 --- a/app/services/pls.py +++ b/app/services/pls.py @@ -118,3 +118,13 @@ async def get_fluentbit_config() -> dict | None: async def put_fluentbit_config(config: dict) -> dict | None: data = await _put("fluent-bit/config", config) return data if isinstance(data, dict) else None + + +async def get_log_config(host: str | None = None) -> dict | None: + data = await _get("mgmt/config/logs", host=host) + return data if isinstance(data, dict) else None + + +async def put_log_config(config: dict, host: str | None = None) -> dict | None: + data = await _put("mgmt/config/logs", config, host=host) + return data if isinstance(data, dict) else None diff --git a/app/ui/index.html b/app/ui/index.html index 942d91c..243824b 100644 --- a/app/ui/index.html +++ b/app/ui/index.html @@ -70,11 +70,20 @@ header h1 span { color: var(--muted); font-weight: 400; } letter-spacing: .1em; color: var(--muted); margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between; } +.section-head { + display: flex; align-items: center; justify-content: space-between; gap: 10px; +} .refresh-btn { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 13px; padding: 1px 4px; border-radius: 4px; transition: color .15s; } .refresh-btn:hover { color: var(--text); } +.collapse-btn { + background: none; border: none; color: var(--muted); cursor: pointer; + font-size: 12px; border-radius: 4px; transition: color .15s, transform .15s; +} +.collapse-btn:hover { color: var(--text); } +.collapsed .collapse-btn { transform: rotate(-90deg); } /* NF grid */ .nf-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 7px; } @@ -192,8 +201,16 @@ header h1 span { color: var(--muted); font-weight: 400; } border-radius: 6px; padding: 7px 8px; white-space: pre-wrap; } +.cluster-flyout { + margin-top: 12px; + border-top: 1px solid rgba(255,255,255,.05); + padding-top: 12px; +} +.cluster-flyout.hidden, +.cluster-flyout[hidden] { display: none !important; } + /* ── Chat panel ─────────────────────────────────────────────────── */ -.chat { display: grid; grid-template-rows: auto auto minmax(0,1fr) auto; overflow: hidden; } +.chat { display: grid; grid-template-rows: auto minmax(0,1fr) auto auto; overflow: hidden; } .messages { min-height: 0; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 14px; } @@ -225,11 +242,15 @@ header h1 span { color: var(--muted); font-weight: 400; } @keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}} /* Suggestions */ -.chips { display: flex; gap: 6px; padding: 0 20px 10px; overflow-x: auto; flex-shrink: 0; } +.chips { + display: flex; gap: 6px; padding: 8px 20px; overflow-x: auto; flex-shrink: 0; + border-top: 1px solid rgba(255,255,255,.04); + background: rgba(15,17,23,.75); +} .chips::-webkit-scrollbar { display: none; } .chip { background: var(--card); border: 1px solid var(--border); border-radius: 20px; - color: var(--text); padding: 5px 13px; font-size: 12px; cursor: pointer; + color: var(--text); padding: 4px 10px; font-size: 11px; line-height: 1.2; cursor: pointer; white-space: nowrap; transition: border-color .15s, background .15s; } .chip:hover { border-color: var(--purple); background: var(--purple-dim); } @@ -261,10 +282,13 @@ header h1 span { color: var(--muted); font-weight: 400; } flex-shrink: 0; display: flex; flex-direction: column; - min-height: 220px; - max-height: 280px; + min-height: 72px; + max-height: 320px; border-bottom: 1px solid var(--border); } +.trace-panel.collapsed { max-height: 72px; } +.trace-panel.collapsed .trace-controls, +.trace-panel.collapsed .trace-log { display: none; } .trace-header { padding: 12px 20px 10px; display: flex; align-items: center; justify-content: space-between; gap: 10px; @@ -274,6 +298,12 @@ header h1 span { color: var(--muted); font-weight: 400; } font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); } .trace-status { font-size: 11px; color: var(--muted); } +.trace-head-left { display: flex; align-items: center; gap: 10px; } +.trace-head-right { display: flex; align-items: center; gap: 12px; } +.refresh-mode { + background: var(--card); color: var(--text); border: 1px solid var(--border); + border-radius: 8px; padding: 5px 8px; font: inherit; +} .trace-controls { padding: 10px 20px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; @@ -311,6 +341,7 @@ header h1 span { color: var(--muted); font-weight: 400; } .left { max-height: 260px; } .trace-controls { grid-template-columns: 1fr 1fr; } .trace-panel { max-height: 320px; } + .trace-head-right { flex-wrap: wrap; justify-content: flex-end; } } @@ -326,19 +357,22 @@ header h1 span { color: var(--muted); font-weight: 400; }
-
-
- Cluster Overview - +
+
+ Cluster Overview +
+ + +
Β·Β·Β·
-
-
-
Discovered Nodes
-
-
Loading cluster inventory…
+
+
Discovered Nodes
+
+
Loading cluster inventory…
+
@@ -349,10 +383,23 @@ header h1 span { color: var(--muted); font-weight: 400; }
-
+
-
Live Log Trace
-
Waiting for log stream…
+
+
Live Log Trace
+ +
+
+ + +
Waiting for log stream…
+
@@ -401,6 +448,10 @@ const ROLE_LABELS = { let latestCluster = { nodes: [] }; let allowedTraceNfs = []; let tracePollHandle = null; +let traceRefreshMode = 'immediate'; +let initialAiMessage = null; +let openNodeKeys = new Set(); +let nodeExpansionInitialized = false; function md(text) { // minimal markdown: **bold**, `code`, newlines @@ -427,6 +478,12 @@ function addMsg(role, html, isTyping=false) { return el; } +function replaceInitialAiMessage(html) { + if (!initialAiMessage) return; + const bubble = initialAiMessage.querySelector('.bubble'); + if (bubble) bubble.innerHTML = html; +} + // ── Network status ───────────────────────────────────────────────────────── async function loadNFs() { try { @@ -446,6 +503,16 @@ async function loadNFs() { populateTraceFilters(d.cluster); $('dot').className = 'dot'; $('connLabel').textContent = 'Live'; + const up = d.summary?.up ?? 0; + const down = d.summary?.down ?? 0; + const nodes = d.cluster?.nodes?.length ?? 0; + replaceInitialAiMessage(md( + `**P5G Marvis is live.**\n\n` + + `Cluster nodes discovered: **${nodes}**\n` + + `Network functions up: **${up}**\n` + + `Network functions down: **${down}**\n\n` + + `Ask about health, alerts, sessions, or logs below.` + )); } catch { $('dot').className = 'dot err'; $('connLabel').textContent = 'Unreachable'; @@ -474,7 +541,39 @@ function populateTraceFilters(cluster) { } function toggleNodeCard(button) { - button.closest('.node-card')?.classList.toggle('open'); + const card = button.closest('.node-card'); + if (!card) return; + const key = card.dataset.nodeKey; + card.classList.toggle('open'); + if (card.classList.contains('open')) { + openNodeKeys.add(key); + } else { + openNodeKeys.delete(key); + } +} + +function toggleClusterFlyout() { + const section = $('clusterSection'); + const flyout = $('clusterFlyout'); + const toggle = $('clusterToggle'); + const isOpen = !flyout.hasAttribute('hidden'); + if (isOpen) { + flyout.setAttribute('hidden', ''); + flyout.classList.add('hidden'); + section.classList.add('collapsed'); + toggle.textContent = 'β–Έ'; + toggle.setAttribute('aria-expanded', 'false'); + return; + } + flyout.removeAttribute('hidden'); + flyout.classList.remove('hidden'); + section.classList.remove('collapsed'); + toggle.textContent = 'β–Ύ'; + toggle.setAttribute('aria-expanded', 'true'); +} + +function toggleTracePanel() { + $('tracePanel').classList.toggle('collapsed'); } function renderNodes(cluster) { @@ -485,29 +584,41 @@ function renderNodes(cluster) { return; } + if (!nodeExpansionInitialized) { + openNodeKeys = new Set( + nodes + .filter(node => node.current) + .map(node => node.hostname || node.address || node.name) + ); + nodeExpansionInitialized = true; + } + list.innerHTML = nodes.map(node => { + const nodeKey = node.hostname || node.address || node.name; const role = ROLE_LABELS[node.role] || node.role || 'AP'; const repoChips = (node.repositories || []).slice(0, 3).map(repo => `${repo.type}:${repo.role}` ).join(''); const running = (node.started_services || []).filter(name => !['alertmanager','prometheus','ncm','pls','fluent-bit','grafana','openvpn','ssh','node-exporter','podman-exporter','licensed','webconsole'].includes(name)); const serviceText = running.length ? running.join(', ') : 'No managed NFs started'; + const expectedSet = new Set((node.expected_nfs || []).map(nf => String(nf).toUpperCase())); const expected = (node.expected_nfs || []).join(', ') || 'No NF profile mapped'; - const nfTiles = (node.nfs || []).map(nf => ` + const profileNfs = (node.nfs || []).filter(nf => expectedSet.has(String(nf.name).toUpperCase())); + const nfTiles = profileNfs.map(nf => `
${nf.name}
${nf.state === 'up' ? '● up' : nf.state === 'down' ? '● dn' : 'β—‹ n/a'}
`).join(''); - const downCount = (node.nfs || []).filter(nf => nf.state === 'down').length; - const openClass = node.current ? 'open' : ''; + const downCount = profileNfs.filter(nf => nf.state === 'down').length; + const openClass = openNodeKeys.has(nodeKey) ? 'open' : ''; return ` -
+
${node.hostname}
-
${node.address} Β· ${node.nfs.filter(nf => nf.state === 'up').length} up${downCount ? `, ${downCount} down` : ''}
+
${node.address} Β· ${profileNfs.filter(nf => nf.state === 'up').length} up${downCount ? `, ${downCount} down` : ''}
${role}${node.current ? ' Β· local' : ''}
β–Ύ
@@ -519,7 +630,7 @@ function renderNodes(cluster) {
Running: ${serviceText}
Profile: ${expected}
-
${nfTiles || '
No node-scoped NF data
'}
+
${nfTiles || '
No profile-scoped NF data
'}
`; @@ -612,6 +723,17 @@ function escapeHtml(value) { async function refresh() { await Promise.all([loadNFs(), loadAlerts(), loadTraces()]); } +function setTraceRefreshMode(mode) { + traceRefreshMode = mode; + if (tracePollHandle) { + clearInterval(tracePollHandle); + tracePollHandle = null; + } + if (mode === 'off') return; + const intervalMs = mode === 'immediate' ? 2000 : parseInt(mode, 10) * 1000; + tracePollHandle = setInterval(loadTraces, intervalMs); +} + // ── Chat ────────────────────────────────────────────────────────────────── async function send() { const inp = $('inp'); @@ -644,15 +766,15 @@ function ask(btn) { $('inp').value = btn.textContent; send(); } // ── Init ────────────────────────────────────────────────────────────────── (async () => { - addMsg('ai', md( + initialAiMessage = addMsg('ai', md( "Hello! I'm **P5G Marvis** β€” your AI assistant for HPE Private 5G.\n\n" + "I monitor your network functions in real time, surface active alerts, and answer " + "natural language questions about your network.\n\n" + - "_Loading network state…_" + "_Connecting to network state…_" )); await refresh(); - setInterval(refresh, 30000); - tracePollHandle = setInterval(loadTraces, 5000); + setInterval(() => Promise.all([loadNFs(), loadAlerts()]), 30000); + setTraceRefreshMode($('traceRefreshMode').value); })(); diff --git a/config/marvis.env.example b/config/marvis.env.example index bfbd102..9e19991 100644 --- a/config/marvis.env.example +++ b/config/marvis.env.example @@ -18,12 +18,16 @@ MARVIS_LOG_RECEIVER_HOST= MARVIS_LOG_RECEIVER_PORT=5514 MARVIS_LOG_RECEIVER_FORMAT=json_lines MARVIS_LOG_BUFFER_LINES=1000 +MARVIS_LOG_PROCESS_BUFFER_LINES=500 +MARVIS_LOG_SUBSCRIBER_BUFFER_LINES=500 MARVIS_LOG_TRACE_BUFFER_LINES=5000 MARVIS_LOG_ALERT_CONTEXT_BEFORE=5 MARVIS_LOG_ALERT_CONTEXT_AFTER=5 MARVIS_LOG_ALERT_CONTEXT_DB_PATH=/app/data/marvis-alert-context.db MARVIS_LOG_ALERT_CONTEXT_DB_MAX_ROWS=500 MARVIS_LOG_FLUENTBIT_MATCH=* +MARVIS_LOG_TRACE_DEBUG_LEVEL=debug +MARVIS_LOG_TRACE_TARGET_SERVICES=amf,smf,mme,upf,sgwc MARVIS_LOG_ALLOWED_NFS=AMF,SMF,UPF,UDM,UDR,NRF,AUSF,PCF,MME,SGWC,DRA,DSM,AAA,BMSC,CHF,SMSF,EIR # AI backend configuration. diff --git a/config/p5g-marvis.service b/config/p5g-marvis.service index d964277..bd4975b 100644 --- a/config/p5g-marvis.service +++ b/config/p5g-marvis.service @@ -23,12 +23,16 @@ Environment=MARVIS_LOG_RECEIVER_HOST= Environment=MARVIS_LOG_RECEIVER_PORT=5514 Environment=MARVIS_LOG_RECEIVER_FORMAT=json_lines Environment=MARVIS_LOG_BUFFER_LINES=1000 +Environment=MARVIS_LOG_PROCESS_BUFFER_LINES=500 +Environment=MARVIS_LOG_SUBSCRIBER_BUFFER_LINES=500 Environment=MARVIS_LOG_TRACE_BUFFER_LINES=5000 Environment=MARVIS_LOG_ALERT_CONTEXT_BEFORE=5 Environment=MARVIS_LOG_ALERT_CONTEXT_AFTER=5 Environment=MARVIS_LOG_ALERT_CONTEXT_DB_PATH=/app/data/marvis-alert-context.db Environment=MARVIS_LOG_ALERT_CONTEXT_DB_MAX_ROWS=500 Environment=MARVIS_LOG_FLUENTBIT_MATCH=* +Environment=MARVIS_LOG_TRACE_DEBUG_LEVEL=debug +Environment=MARVIS_LOG_TRACE_TARGET_SERVICES=amf,smf,mme,upf,sgwc Environment=MARVIS_LOG_ALLOWED_NFS=AMF,SMF,UPF,UDM,UDR,NRF,AUSF,PCF,MME,SGWC,DRA,DSM,AAA,BMSC,CHF,SMSF,EIR Environment=MARVIS_AI_MODE=rule Environment=MARVIS_OPENAI_API_KEY= @@ -58,12 +62,16 @@ ExecStart=/usr/bin/docker run \ --env MARVIS_LOG_RECEIVER_PORT \ --env MARVIS_LOG_RECEIVER_FORMAT \ --env MARVIS_LOG_BUFFER_LINES \ + --env MARVIS_LOG_PROCESS_BUFFER_LINES \ + --env MARVIS_LOG_SUBSCRIBER_BUFFER_LINES \ --env MARVIS_LOG_TRACE_BUFFER_LINES \ --env MARVIS_LOG_ALERT_CONTEXT_BEFORE \ --env MARVIS_LOG_ALERT_CONTEXT_AFTER \ --env MARVIS_LOG_ALERT_CONTEXT_DB_PATH \ --env MARVIS_LOG_ALERT_CONTEXT_DB_MAX_ROWS \ --env MARVIS_LOG_FLUENTBIT_MATCH \ + --env MARVIS_LOG_TRACE_DEBUG_LEVEL \ + --env MARVIS_LOG_TRACE_TARGET_SERVICES \ --env MARVIS_LOG_ALLOWED_NFS \ --env MARVIS_AI_MODE \ --env MARVIS_OPENAI_API_KEY \